【问题标题】:How can I do 'if..else' inside a for-comprehension?我怎样才能在理解中做“if..else”?
【发布时间】:2012-11-13 15:48:17
【问题描述】:

我问了一个非常基本的问题,最近让我很困惑。 我想编写一个 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 风格重写代码?

【问题讨论】:

  • 第一个例子有什么问题?
  • 你的问题“标题”对于你定义的问题来说太可悲了;)
  • 我来这个问题是因为从标题中我想我会听到由于存在理解而在语言中引入的问题。可爱的标题在博客中很有趣,但为了让读者有效地使用 StackOverflow 作为解决问题的资源,标题传达问题的含义很重要。这个问题的一个好标题是“Need 'if..else' inside for-comprehension”。
  • 哇,令人惊讶的是,没有人根据可怕的标题投票反对或投票结束。编辑它。

标签: scala for-comprehension


【解决方案1】:

您编写的第一个代码完全有效,因此无需重写。在其他地方,您说您想知道如何以 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)

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

第一个 sn-p 将通过两个 map 调用实现,而第二个 sn-p 将使用一个 flatMap 和一个 map

因此,只有在yield 的上下文中,才需要担心嵌套for 循环或使用多个生成器。而且,事实上,generators 代表某物正在生成这一事实,这仅适用于真正的理解(那些yielding 的东西)。

【讨论】:

  • 你的回答很有帮助,我可以说,如果我不使用'for'返回值,我最好不要使用生成器语法,而是使用其他语法?
  • @ZZcat 并不是说​​在不使用yield 时使用一种或另一种语法“更好”。只是这两种方式都没有关系,所以您可以选择最适合您的方式。
【解决方案2】:

部分

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 代替)

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

【讨论】:

  • 我一直怀疑较短的代码是否真的比较长的代码运行得更快,一些嵌套的高阶函数具有 O(n 平方) 复杂度,但它们看起来确实很短。人们大量使用这些高阶函数,我认为这可能是有害的。我不确定我是否正确,欢迎对我的观点进行任何更正。
  • @ZZCat 性能对您的用例有多重要,您映射了多少个条目?
  • @Kevin,不过,题外话,这不是关于有多少个条目,而是关于你在整个 Scala 编程生命中编写了多少个低效的 For 循环。好吧,“更快”是一个很有争议的词,我会谨慎使用它。
  • @ZZCat 这也是关于过早优化是万恶之源。通过清晰简洁地查看算法(然后看到更好的整体设计)而不是在循环级别进行早期微优化,您通常可以获得更好的性能提升
  • @ZZcat 好吧,我假设这个复杂的代码与一行代码一起使用:方法调用。如果到处都是复制和粘贴,那么您还有其他问题。如果它与方法调用一起使用,并且使用它的程序员通过查看实现而不是文档来评估其复杂性,那么您也会遇到其他问题。
【解决方案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

【讨论】:

    【解决方案4】:

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

    【讨论】:

    • 我知道过滤器表达式不能有 else 语句,我想知道这部分逻辑是如何用 Scala 风格表达的。
    • 是的,这只是意味着您要在此处使用的 monad-for 在您的情况下不是最简单的解决方案。这并不意味着它是一种语法毒药,它只是不是合适的工具。
    • 是的,没那么糟糕,我只是想听听更多人对此的看法。我的例子不是一个特例,我认为它在任何编程语言中都很常见。
    • 此答案仅与第二个代码示例有关。你不能在 for 表达式生成器中使用 else 语句,但你可以在正文中使用 if/else,就像在第一个代码中一样
    【解决方案5】:

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

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

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

    【讨论】:

      【解决方案6】:

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

      这意味着如果要基于条件表达式执行替代操作,则需要将测试推迟到 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()
      }
      

      【讨论】:

      • 这绝对不会比命令式风格更好看。而且似乎 For 表达式的所有“语法糖”只要性能不佳就毫无意义。
      • @ZZcat,我认为 for 构造(理解)非常有用,它能够进行多级迭代、过滤和中间赋值,从而导致结果集合的可选返回,不幸的是,我认为它是与您上面提供的代码不太自然。
      • 是的,我同意“多级迭代”和“过滤”,只是找不到容易做一些“副作用”的方法。在 File 和 IO 编程中,我总是想对文件做一些操作,而不关心返回值。我几乎找不到地方放置我的代码来完成操作。
      • 谢谢,很抱歉造成误导,两个doSomethingElse()不必相同,我已经更新了我的代码,真的很抱歉。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-05-25
      • 1970-01-01
      • 2022-08-19
      • 2012-12-26
      • 2019-09-01
      • 1970-01-01
      • 2012-04-20
      相关资源
      最近更新 更多