24

我问了一个非常基本的问题,最近让我很困惑。我想编写一个 Scala For 表达式来执行以下操作:

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

问题是,在多个生成器的 For 表达式中,我不知道我可以将每个 for 表达式主体放在哪里。

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

如何以 Scala 风格重写代码?

4

6 回答 6

22

您编写的第一个代码完全有效,因此无需重写。在其他地方,您说您想知道如何以 Scala 风格进行操作。没有真正的“Scala 风格”,但我会假设一种更实用的风格并加以解决。

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

第一个问题是 this 没有返回值。它所做的只是副作用,这也是要避免的。所以第一个变化是这样的:

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)

的类型resultArray[AnyRef],而使用多个生成器会产生Array[Element]。修复的简单部分是这样的:

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

但仅此一项是行不通的,因为分类元素本身返回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(见这里,或其他类似的问题和答案)实现的。这意味着上面的代码与此代码完全相同:

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

完全一样的东西。当一个人使用yield. 以下表达式不会产生相同的结果:

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

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

第一个片段将通过两个map调用实现,而第二个片段将使用 oneflatMap和 one map

因此,只有在这种情况下yield,担心嵌套for循环或使用多个生成器才有意义。而且,实际上,生成器代表正在生成某物的事实,这仅适用于真正的理解(the one 正在生成yield某物)。

于 2010-11-16T17:31:36.917 回答
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 代替)

在这里它不是那么有用,但如果你有更深的嵌套结构,它可以......

于 2010-11-16T10:53:57.087 回答
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
于 2010-11-16T20:42:14.983 回答
2

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

于 2010-11-16T08:28:24.523 回答
1

如果顺序对于调用 doSomething() 和 doSomethingElse() 并不重要,那么您可以像这样重新排列代码。

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

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

为了回答您最初的问题,我认为对于特定用例来说,理解可能非常好,而您的示例不太适合。

于 2010-11-16T13:47:16.817 回答
0

Scala 中为操作指定的条件用于过滤来自生成器的元素。不满足条件的元素将被丢弃,并且不会呈现给产量/代码块。

这意味着如果要基于条件表达式执行替代操作,则需要将测试推迟到 yield / 代码块。

另请注意, for 操作的计算(当前)相对昂贵,因此可能更简单的迭代方法可能更合适,可能类似于:

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

更新:

如果你必须使用 for 理解并且你可以忍受一些限制,这可能会起作用:

for (i <- expr1; j <- i) {
  if (i.method) {if (j.method) doSomething() else doSomethingElseA()} else doSomethingElseB()
}
于 2010-11-16T09:32:37.370 回答