17

我对 Scala 有点陌生,所以如果这有点微不足道,我深表歉意。

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

我习惯于for - yield过滤通过某些生成器(项目列表、序列等)生成的项目的语法。然而,就我而言,我只想在不执行其余循环的情况下中断并返回 false。在普通的 Java 中,只需return false;在循环内执行 a 即可。

以一种低效的方式(即当我遇到第一个错误项目时不停止),我可以这样做:

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

这实质上是说,如果没有项目通过过滤器,则所有项目都满足条件。但这似乎有点令人费解且效率低下(假设您有 100 万个项目,而第一个项目已经不满足条件)。

在 Scala 中最好和最优雅的方法是什么?

4

4 回答 4

26

forall在 Scala 中使用在条件的第一个 false 处尽早停止。(一个相关的问题

您的解决方案重写:

items.forall(satisfiesCondition)

演示短路:

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

与之相反的forallexists,一旦满足条件就停止:

List(1,2,3,4,5,6).exists{ x => println(x); x > 3 }
1
2
3
4
res2: Boolean = true
于 2013-11-14T15:54:56.000 回答
5

Scala 的 for 理解不是一般的迭代。这意味着它们不能产生一个可以从迭代中产生的每一个可能的结果,例如,你想做的事情。

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

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

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

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

该表达式char.toInt将 a 转换Char为 a Int,因此在 ScalaString中隐式转换为 aSeq[Char]的 - 变为 a Seq[Int](实际上,IndexedSeq[Int]通过一些 Scala 集合魔法,是 an )。

它可以做的第二件事是:

  • 给定 , , 等类型的对象,以及 , ,M[A]等类型的函数into ,返回一个类型为 的对象;M[B]M[C]ABCDM[D]

您可以将其视为对先前转换的概括,尽管并非所有可以支持先前转换的东西都必然支持此转换。例如,我们可以为战舰游戏的所有坐标生成坐标,如下所示:

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

在这种情况下,我们有 和 类型的对象Seq[Char]Seq[Int]还有一个函数(Char, Int) => String,所以我们得到了一个Seq[String]

第三个也是最后一个 for 理解可以做的事情是:

  • 给定一个类型的对象M[A],使得该类型对于任何类型、函数和条件M[T]都具有值,根据条件返回或类型的对象;TA => BA => BooleanM[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,我们得到一个结果,而且似乎不需要任何类型的“零”。在这种情况下,零将是空序列,但似乎几乎不值得一提。

为了更好地解释它,我将从 切换SeqOption。AnOption[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 理解需要返回一些东西,而那个东西是“零”:None

用技术术语来说,Scala 的 for comprehension 为 monad 上的操作提供了语法糖,并为monad提供了一个额外的操作为零(请参阅同一篇文章中的列表理解)。

您实际想要完成的称为catamorphism,通常表示为一种fold方法,可以将其视为 的函数M[A] => B。您可以使用或按顺序编写它fold,但它们都不会真正使迭代短路。foldLeftfoldRight

短路自然产生于非严格评估,这是 Haskell 中的默认设置,大多数论文都是在其中编写的。Scala 与大多数其他语言一样,默认情况下是严格的。

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

  1. 使用针对您的精确用例的特殊方法forallor exists,尽管它们不能解决一般问题;
  2. 使用非严格的集合;有 Scala's Stream,但它存在阻碍其有效使用的问题。Scalaz库可以为您提供帮助
  3. 使用提前返回,这是 Scala 库在一般情况下解决此问题的方式(在特定情况下,它使用更好的优化)。

作为第三个选项的示例,您可以这样写:

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 实验,使用了论文中描述的同名概念。

于 2013-11-14T22:36:10.027 回答
2

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

对于不涉及集合的短路用例,我倾向于大量使用递归。

于 2013-11-15T03:05:56.323 回答
0

更新:

不要在我下面的答案中使用代码!

在我发布下面的答案后不久(在误解了原始发帖人的问题之后),我在这里发现了一种更好的通用答案(对下面的要求列表):https ://stackoverflow.com/a/60177908/501113


看来您有几个要求:

  1. 遍历一个(可能很大的)项目列表做一些(可能是昂贵的)工作
  2. 对项目所做的工作可能会返回错误
  3. 在第一个返回错误的 item 处,将迭代短路,丢弃已经完成的工作,并返回 item 的错误

A for comprehension 不是为此设计的(如其他答案中所述)。

而且我找不到提供上述要求的另一个 Scala 集合预构建迭代器。

虽然下面的代码基于一个人为的示例(将 aString数字转换为 a BigInt),但它是我更喜欢使用的一般模式;即处理一个集合并将其转换为其他东西。

def getDigits(shouldOnlyBeDigits: String): Either[IllegalArgumentException, BigInt] = {
  @scala.annotation.tailrec
  def recursive(
      charactersRemaining: String = shouldOnlyBeDigits
    , accumulator: List[Int] = Nil
  ): Either[IllegalArgumentException, List[Int]] =
    if (charactersRemaining.isEmpty)
      Right(accumulator) //All work completed without error
    else {
      val item = charactersRemaining.head
      val isSuccess =
        item.isDigit //Work the item
      if (isSuccess)
        //This item's work completed without error, so keep iterating
        recursive(charactersRemaining.tail, (item - 48) :: accumulator)
      else {
        //This item hit an error, so short circuit
        Left(new IllegalArgumentException(s"item [$item] is not a digit"))
      }
    }
  recursive().map(digits => BigInt(digits.reverse.mkString))
}

getDigits("1234")在 REPL(或 Scala 工作表)中调用它时,它会返回: val res0: Either[IllegalArgumentException,BigInt] = Right(1234)

getDigits("12A34")在 REPL(或 Scala 工作表)中调用时,它返回: val res1: Either[IllegalArgumentException,BigInt] = Left(java.lang.IllegalArgumentException: item [A] is not digit)

你可以在这里在 Scastie 中玩这个: https ://scastie.scala-lang.org/7ddVynRITIOqUflQybfXUA

于 2021-09-25T19:31:28.057 回答