6

When building up a collection inside an Option, each attempt to make the next member of the collection might fail, making the collection as a whole a failure, too. Upon the first failure to make a member, I'd like to give up immediately and return None for the whole collection. What is an idiomatic way to do this in Scala?

Here's one approach I've come up with:

def findPartByName(name: String): Option[Part] = . . .

def allParts(names: Seq[String]): Option[Seq[Part]] =
  names.foldLeft(Some(Seq.empty): Option[Seq[Part]]) {
    (result, name) => result match {
      case Some(parts) =>
        findPartByName(name) flatMap { part => Some(parts :+ part) }
      case None => None
    }
  }

In other words, if any call to findPartByName returns None, allParts returns None. Otherwise, allParts returns a Some containing a collection of Parts, all of which are guaranteed to be valid. An empty collection is OK.

The above has the advantage that it stops calling findPartByName after the first failure. But the foldLeft still iterates once for each name, regardless.

Here's a version that bails out as soon as findPartByName returns a None:

def allParts2(names: Seq[String]): Option[Seq[Part]] = Some(
  for (name <- names) yield findPartByName(name) match {
    case Some(part) => part
    case None => return None
  }
)

I currently find the second version more readable, but (a) what seems most readable is likely to change as I get more experience with Scala, (b) I get the impression that early return is frowned upon in Scala, and (c) neither one seems to make what's going on especially obvious to me.

The combination of "all-or-nothing" and "give up on the first failure" seems like such a basic programming concept, I figure there must be a common Scala or functional idiom to express it.

4

5 回答 5

5

你的return代码中实际上是匿名函数的几个层次。因此,它必须通过抛出在外部函数中捕获的异常来实现。这既不高效也不漂亮,因此皱着眉头。

while循环和Iterator.

def allParts3(names: Seq[String]): Option[Seq[Part]] = {
  val iterator = names.iterator
  var accum = List.empty[Part]
  while (iterator.hasNext) {
    findPartByName(iterator.next) match {
      case Some(part) => accum +:= part
      case None => return None
    }
  }
  Some(accum.reverse)
}

因为我们不知道是什么类型,Seq names所以我们必须创建一个迭代器来有效地循环它,而不是使用tailor 索引。while 循环可以用尾递归内部函数替换,但使用迭代器,while循环更清晰。

于 2014-05-28T15:25:25.690 回答
4

Scala 集合有一些选项可以使用惰性来实现这一点。

您可以使用viewtakeWhile

def allPartsWithView(names: Seq[String]): Option[Seq[Part]] = {
    val successes = names.view.map(findPartByName)
                              .takeWhile(!_.isEmpty)
                              .map(_.get)
                              .force
    if (!names.isDefinedAt(successes.size)) Some(successes)
    else None
}

在早期失败的情况下,使用ifDefinedAt可以避免潜在地遍历长输入。names

您也可以使用toStreamandspan来实现相同的目的:

def allPartsWithStream(names: Seq[String]): Option[Seq[Part]] = {
    val (good, bad) = names.toStream.map(findPartByName)
                                    .span(!_.isEmpty)
    if (bad.isEmpty) Some(good.map(_.get).toList)
    else None
}

我发现尝试混合viewspan导致findPartByName在成功的情况下对每个项目进行两次评估。

但是,如果发生任何错误,则返回错误条件的整个想法听起来更像是一项用于抛出和捕获异常的工作(“该”工作?)。我想这取决于您程序中的上下文。

于 2014-05-28T15:40:02.463 回答
3

结合其他答案,即可变标志与地图和 takeWhile 我们喜欢。

给定一个无限流:

scala> var count = 0
count: Int = 0

scala> val vs = Stream continually { println(s"Compute $count") ; count += 1 ; count }
Compute 0
vs: scala.collection.immutable.Stream[Int] = Stream(1, ?)

直到谓词失败:

scala> var failed = false
failed: Boolean = false

scala> vs map { case x if x < 5 => println(s"Yup $x"); Some(x) case x => println(s"Nope $x"); failed = true; None } takeWhile (_.nonEmpty) map (_.get)
Yup 1
res0: scala.collection.immutable.Stream[Int] = Stream(1, ?)

scala> .toList
Compute 1
Yup 2
Compute 2
Yup 3
Compute 3
Yup 4
Compute 4
Nope 5
res1: List[Int] = List(1, 2, 3, 4)

或更简单地说:

scala> var count = 0
count: Int = 0

scala> val vs = Stream continually { println(s"Compute $count") ; count += 1 ; count }
Compute 0
vs: scala.collection.immutable.Stream[Int] = Stream(1, ?)

scala> var failed = false
failed: Boolean = false

scala> vs map { case x if x < 5 => println(s"Yup $x"); x case x => println(s"Nope $x"); failed = true; -1 } takeWhile (_ => !failed)
Yup 1
res3: scala.collection.immutable.Stream[Int] = Stream(1, ?)

scala> .toList
Compute 1
Yup 2
Compute 2
Yup 3
Compute 3
Yup 4
Compute 4
Nope 5
res4: List[Int] = List(1, 2, 3, 4)
于 2014-05-28T17:45:22.560 回答
2

我认为您的allParts2函数有问题,因为 match 语句的两个分支之一会产生副作用。该return语句不是惯用的,表现得好像您正在执行命令式跳转。

第一个函数看起来更好,但如果您担心 foldLeft 可能产生的次优迭代,您可能应该采用如下递归解决方案:

def allParts(names: Seq[String]): Option[Seq[Part]] = {
  @tailrec
  def allPartsRec(names: Seq[String], acc: Seq[String]): Option[Seq[String]] = names match {
    case Seq(x, xs@_*) => findPartByName(x) match {
      case Some(part) => allPartsRec(xs, acc +: part)
      case None => None
    }
    case _ => Some(acc)
  }

  allPartsRec(names, Seq.empty)
}

我没有编译/运行它,但这个想法应该存在,我相信它比使用返回技巧更惯用!

于 2014-05-28T15:08:20.580 回答
1

我一直认为这必须是一个或两个班轮。我想出了一个:

def allParts4(names: Seq[String]): Option[Seq[Part]] = Some(
  names.map(findPartByName(_) getOrElse { return None })
)

优势:

  • 意图非常明确。没有混乱,也没有异国情调或非标准的 Scala。

缺点:

  • return正如 Aldo Stracquadanio 指出的那样,早期违反了参考透明度。你不能在allParts4不改变其含义的情况下将其主体放入其调用代码中。

  • 正如wingedsubmariner 指出的那样,由于内部抛出和捕获异常,可能效率低下。

果然,我把它放入了一些真实的代码中,在十分钟内,我将表达式包含在其他东西中,并且可以预见地得到令人惊讶的行为。所以现在我明白了为什么提早return不受欢迎。

这是一个非常常见的操作,在大量使用 的代码中非常重要Option,而且 Scala 通常非常擅长组合事物,我不敢相信没有一个非常自然的习惯用法可以正确地做到这一点。

单子不是很好地指定如何组合动作吗?有GiveUpAtTheFirstSignOfResistance单子吗?

于 2014-05-29T09:59:38.857 回答