当对其中一个项目的检查返回false时,结束for-comprehension循环

时间:2013-11-14 15:22:13

标签: scala for-comprehension

我对Scala有点新意,如果这有点微不足道,请道歉。

我有一个我想要迭代的项目列表。我对每个项目执行检查,如果只有其中一个失败,我希望整个函数返回false。因此,您可以将此视为AND条件。我希望它被懒惰地评估,即我遇到第一个错误返回假的那一刻。

我习惯于for - yield语法,它过滤通过某个生成器生成的项目(项目列表,序列等)。在我的情况下,我只是想突破并返回false而不执行其余的循环。在普通的Java中,只需在循环中执行return false;

以低效的方式(即当我遇到第一个假项目时没有停止),我可以这样做:

   (for {
          item <- items
          if !satisfiesCondition(item)
        } yield item).isEmpty

基本上说,如果没有物品通过过滤器,所有物品都满足条件。但这似乎有点复杂和低效(考虑到你有100万件物品,第一件已经不满足条件)。

在Scala中执行此操作的最佳和最优雅的方法是什么?

3 个答案:

答案 0 :(得分:22)

在Scala中使用forall在条件的第一个错误处提前停止。 (A related question

您的解决方案已重写:

items.forall(satisfiesCondition)

证明短路:

List(1,2,3,4,5,6).forall { x => println(x); x < 3 }
1
2
3
res1: Boolean = false

forall相反的是exists,只要满足条件就会停止:

List(1,2,3,4,5,6).exists{ x => println(x); x > 3 }
1
2
3
4
res2: Boolean = true

答案 1 :(得分:5)

Scala的理解不是一般的迭代。这意味着他们无法产生一个人可以通过迭代产生的每一个可能的结果,例如,你想要做的事情。

当你返回一个值(即使用yield)时,Scala可以做三件事。在最基本的情况下,它可以这样做:

  • 给定类型M[A]的对象和函数A => B(即,当给定类型B的对象时,返回类型为A的对象),返回M[B];
  • 类型的对象

例如,给定一系列字符Seq[Char],获取该字符的UTF-16整数:

val codes = for (char <- "A String") yield char.toInt

表达式char.toIntChar转换为Int,因此String - 在Scala中隐式转换为Seq[Char] - ,通过一些Scala集合魔术成为Seq[Int](实际上是IndexedSeq[Int]。)

它可以做的第二件事是:

  • 给定M[A]M[B]M[C]等类型的对象,以及ABC等的函数进入D,返回M[D];
  • 类型的对象

您可以将此视为先前转换的概括,但并非所有可支持先前转换的内容都必然支持此转换。例如,我们可以为battleship游戏的所有坐标生成坐标,如下所示:

val coords = for {
  column <- 'A' to 'L'
  row    <- 1 to 10
} yield s"$column$row"

在这种情况下,我们有Seq[Char]Seq[Int]类型的对象,以及函数(Char, Int) => String,因此我们返回Seq[String]

理解的第三个也是最后的事情是:

  • 给定M[A]类型的对象,使得M[T]类型对于任何类型T具有值,函数A => B和条件A => Boolean,返回或类型为M[B]的对象,具体取决于条件;

这个很难理解,虽然起初可能看起来很简单。让我们先看看看起来很简单的东西,比如找一系列字符中的所有元音:

def vowels(s: String) = for {
  letter <- s
  if Set('a', 'e', 'i', 'o', 'u') contains letter.toLower
} yield letter.toLower

val aStringVowels = vowels("A String")

看起来很简单:我们有一个条件,我们有一个函数Char => Char,我们得到一个结果,似乎不需要任何类型的“零”。在这种情况下,零将是空序列,但似乎不值得一提。

为了更好地解释,我会从Seq切换到OptionOption[A]有两种子类型:Some[A]None。显然,零是None。当您需要表示可能缺少值或值本身时,可以使用它。

现在,假设我们有一个Web服务器,用户登录并且是管理员在他们的网页上获取额外的javascript用于管理任务(如wordpress)。首先,我们需要获取用户,如果有用户登录,让我们说这是通过这种方法完成的:

def getUser(req: HttpRequest): Option[User]

如果用户未登录,我们会收到None,否则会收到Some(user),其中user是数据结构,其中包含有关发出请求的用户的信息。然后我们可以对此操作进行建模:

def adminJs(req; HttpRequest): Option[String] = for {
  user <- getUser(req)
  if user.isAdmin
} yield adminScriptForUser(user)

这里更容易看到零点。当条件为假时,adminScriptForUser(user)无法执行,因此for comprehension需要返回一些东西,而且某些东西是“零”:None

在技术术语中,Scala的for comprehensions为monads上的操作提供了语法糖,并对monad的零操作进行了额外的操作(参见同一篇文章中的列表推导)。

您实际想要完成的内容称为catamorphism,通常表示为fold方法,可以将其视为M[A] => B的函数。您可以在序列中使用foldfoldLeftfoldRight来编写它,但它们都不会实际上使迭代短路。

短路是由非严格评估引起的,这是Haskell中的默认评估,其中大部分论文都是在其中编写的。与大多数其他语言一样,Scala默认是严格的。

您的问题有三种解决方案:

  1. 使用针对您的精确用例的特殊方法forallexists,但它们无法解决一般问题;
  2. 使用非严格的集合;有Scala的Stream,但它存在妨碍其有效使用的问题。 Scalaz图书馆可以为您提供帮助;
  3. 使用早期返回,这是Scala库在一般情况下解决此问题的方式(在特定情况下,它使用更好的优化)。
  4. 作为第三个选项的示例,您可以这样写:

    def hasEven(xs: List[Int]): Boolean = {
      for (x <- xs) if (x % 2 == 0) return true
      false
    }
    

    请注意,这称为“for循环”,而不是“for comprehension”,因为它不返回值(好吧,它返回Unit),因为它没有yield关键字。

    您可以在文章The Essence of The Iterator Pattern中阅读有关真正通用迭代的更多信息,这是一篇Scala实验,其中包含了论文中描述的相同名称的概念。

答案 2 :(得分:1)

forall绝对是特定场景的最佳选择,但为了说明这里的旧递归:

@tailrec def hasEven(xs: List[Int]): Boolean = xs match {
  case head :: tail if head % 2 == 0 => true
  case Nil  => false
  case _ => hasEven(xs.tail)
}

对于不涉及集合的短路用例,我倾向于使用递归来进行循环。