如何在达到第一个“无”时停止构建选项[集合]?

时间:2014-05-28 14:40:39

标签: scala monads scala-collections optional scala-option

Option内构建集合时,每次尝试创建集合的下一个成员都可能会失败,从而使集合整体失败。在第一次未成为会员时,我想立即放弃并返回None整个集合。在Scala中执行此操作的惯用方法是什么?

这是我提出的一种方法:

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

换句话说,如果对findPartByName的任何回复NoneallParts都会返回None。否则,allParts会返回包含Some集合的Parts,所有这些集合都保证有效。空集合是可以的。

以上优点是在第一次失败后停止调用findPartByName。但是foldLeft仍然会为每个名称迭代一次,无论如何。

这是findPartByName返回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
  }
)

我目前发现第二个版本更具可读性,但是(a)看起来最具可读性的东西可能随着我获得Scala的更多经验而改变,(b)我得到的结论是早期return不赞成斯卡拉,(c)似乎没有一个人能够对我做出特别明显的事情。

&#34;全有或全无&#34;并且&#34;放弃第一次失败&#34;看起来像是一个基本的编程概念,我想必须有一个共同的Scala或功能成语来表达它。

5 个答案:

答案 0 :(得分: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是什么类型,所以我们必须创建一个迭代器来有效地循环它,而不是使用tail或索引。 while循环可以用尾递归内部函数替换,但是使用迭代器,while循环更清晰。

答案 1 :(得分: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

您还可以使用toStreamspan来实现同样的目标:

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每个项目评估两次,以防万一。

如果发生任何错误,返回错误条件的整个想法确实听起来更像是投掷和捕获异常的作业(&#34;&#34;作业?)。我想这取决于你的程序中的上下文。

答案 2 :(得分:3)

将其他答案结合起来,即一张可变的旗帜和地图,然后我们喜欢。

给定无限流:

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)

答案 3 :(得分:2)

我认为你的allParts2函数存在问题,因为匹配语句的两个分支之一会产生副作用。 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)
}

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

答案 4 :(得分:1)

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

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

优势:

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

缺点:

  • 早期的return违反了参照透明度,正如Aldo Stracquadanio指出的那样。您无法将allParts4的正文放入其调用代码中,而不会更改其含义。

  • 由于内部投掷和捕获异常,可能效率低下,如飞行员指出的那样。

果然,我把它放到一些真实的代码中,并且在十分钟之内,我将表达式包含在其他内容中,并且可以预见得到令人惊讶的行为。所以现在我明白为什么早期的return不赞成。

这是一个常见的操作,在代码中非常重要,大量使用Option,而Scala通常很擅长组合事物,我无法相信它不是很自然成语是正确的。

Aren的monad适合指定如何组合动作?是否有GiveUpAtTheFirstSignOfResistance monad?

相关问题