95

提早终止弃牌的最佳方式是什么?作为一个简化的例子,假设我想总结 an 中的数字Iterable,但如果我遇到一些我不期望的东西(比如一个奇数),我可能想要终止。这是第一个近似值

def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
  nums.foldLeft (Some(0): Option[Int]) {
    case (Some(s), n) if n % 2 == 0 => Some(s + n)
    case _ => None
  }
}

然而,这个解决方案非常丑陋(例如,如果我做了一个 .foreach 和一个 return - 它会更干净更清晰),最糟糕的是,即使遇到非偶数,它也会遍历整个迭代.

那么写这样一个提前终止的折叠的最好方法是什么?我应该去递归地写这个,还是有更接受的方式?

4

11 回答 11

69

我的第一选择通常是使用递归。它只是稍微不那么紧凑,可能更快(当然不会更慢),并且在提前终止时可以使逻辑更清晰。在这种情况下,您需要嵌套的 def,这有点尴尬:

def sumEvenNumbers(nums: Iterable[Int]) = {
  def sumEven(it: Iterator[Int], n: Int): Option[Int] = {
    if (it.hasNext) {
      val x = it.next
      if ((x % 2) == 0) sumEven(it, n+x) else None
    }
    else Some(n)
  }
  sumEven(nums.iterator, 0)
}

我的第二个选择是使用return,因为它使其他所有内容保持不变,您只需将折叠包装在 a 中def,这样您就可以从中返回一些东西——在这种情况下,您已经有了一个方法,所以:

def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
  Some(nums.foldLeft(0){ (n,x) =>
    if ((n % 2) != 0) return None
    n+x
  })
}

在这种特殊情况下,它比递归更紧凑(尽管我们在递归方面特别不走运,因为我们必须进行可迭代/迭代器转换)。当所有其他条件都相同时,跳跃的控制流是要避免的,但这里不是。在有价值的情况下使用它没有害处。

如果我经常这样做并希望它在某个方法的中间(所以我不能只使用 return),我可能会使用异常处理来生成非本地控制流。也就是说,它毕竟擅长什么,而且错误处理并不是它唯一有用的时候。唯一的技巧是避免生成堆栈跟踪(这真的很慢),这很容易,因为 traitNoStackTrace和它的子 traitControlThrowable已经为你做到了。Scala 已经在内部使用了它(事实上,这就是它实现从折叠内部返回的方式!)。让我们自己做(不能嵌套,虽然可以解决这个问题):

import scala.util.control.ControlThrowable
case class Returned[A](value: A) extends ControlThrowable {}
def shortcut[A](a: => A) = try { a } catch { case Returned(v) => v }

def sumEvenNumbers(nums: Iterable[Int]) = shortcut{
  Option(nums.foldLeft(0){ (n,x) =>
    if ((x % 2) != 0) throw Returned(None)
    n+x
  })
}

当然在这里使用return更好,但请注意,您可以放在shortcut任何地方,而不仅仅是包装整个方法。

接下来对我来说是重新实现折叠(我自己或找到一个这样做的库),以便它可以发出提前终止的信号。这样做的两种自然方式是不传播值,而是Option包含值,其中None表示终止;或使用第二个指示完成的指标函数。Kim Stebel 展示的 Scalaz 惰性折叠已经涵盖了第一种情况,所以我将展示第二种情况(使用可变实现):

def foldOrFail[A,B](it: Iterable[A])(zero: B)(fail: A => Boolean)(f: (B,A) => B): Option[B] = {
  val ii = it.iterator
  var b = zero
  while (ii.hasNext) {
    val x = ii.next
    if (fail(x)) return None
    b = f(b,x)
  }
  Some(b)
}

def sumEvenNumbers(nums: Iterable[Int]) = foldOrFail(nums)(0)(_ % 2 != 0)(_ + _)

(是否通过递归、返回、惰性等方式实现终止取决于您。)

我认为这涵盖了主要的合理变体;还有一些其他选项,但我不确定为什么在这种情况下会使用它们。(Iterator如果它有 ,它本身会很好用findOrPrevious,但它没有,而且手动完成它所需要的额外工作使它成为在这里使用的一个愚蠢的选择。)

于 2012-10-15T14:37:34.687 回答
27

您描述的场景(在某些不需要的情况下退出)似乎是该takeWhile方法的一个很好的用例。它本质上是filter,但应该在遇到不满足条件的元素时结束。

例如:

val list = List(2,4,6,8,6,4,2,5,3,2)
list.takeWhile(_ % 2 == 0) //result is List(2,4,6,8,6,4,2)

这也适用于Iterators/ Iterables。我为您的“偶数总和,但奇数中断”建议的解决方案是:

list.iterator.takeWhile(_ % 2 == 0).foldLeft(...)

只是为了证明一旦它达到奇数就不会浪费你的时间......

scala> val list = List(2,4,5,6,8)
list: List[Int] = List(2, 4, 5, 6, 8)

scala> def condition(i: Int) = {
     |   println("processing " + i)
     |   i % 2 == 0
     | }
condition: (i: Int)Boolean

scala> list.iterator.takeWhile(condition _).sum
processing 2
processing 4
processing 5
res4: Int = 6
于 2012-10-15T17:17:01.183 回答
14

您可以使用 scalaz 中的 foldRight 的惰性版本以功能样式执行您想要的操作。有关更深入的解释,请参阅此博客文章。虽然此解决方案使用 a Stream,但您可以使用有效地将 a 转换Iterable为 a 。Streamiterable.toStream

import scalaz._
import Scalaz._

val str = Stream(2,1,2,2,2,2,2,2,2)
var i = 0 //only here for testing
val r = str.foldr(Some(0):Option[Int])((n,s) => {
  println(i)
  i+=1
  if (n % 2 == 0) s.map(n+) else None
})

这仅打印

0
1

这清楚地表明匿名函数只被调用了两次(即直到它遇到奇数)。这是由于 foldr 的定义,其签名(在 的情况下Stream)是def foldr[B](b: B)(f: (Int, => B) => B)(implicit r: scalaz.Foldable[Stream]): B. 请注意,匿名函数将按名称参数作为其第二个参数,因此不需要对其进行评估。

顺便说一句,您仍然可以使用 OP 的模式匹配解决方案来编写它,但我发现 if/else 和 map 更优雅。

于 2012-10-15T10:10:37.793 回答
7

好吧,Scala 确实允许非本地返回。对于这是否是一种好的风格,有不同的看法。

scala> def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
     |   nums.foldLeft (Some(0): Option[Int]) {
     |     case (None, _) => return None
     |     case (Some(s), n) if n % 2 == 0 => Some(s + n)
     |     case (Some(_), _) => None
     |   }
     | }
sumEvenNumbers: (nums: Iterable[Int])Option[Int]

scala> sumEvenNumbers(2 to 10)
res8: Option[Int] = None

scala> sumEvenNumbers(2 to 10 by 2)
res9: Option[Int] = Some(30)

编辑:

在这种特殊情况下,正如@Arjan 建议的那样,您还可以这样做:

def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
  nums.foldLeft (Some(0): Option[Int]) {
    case (Some(s), n) if n % 2 == 0 => Some(s + n)
    case _ => return None
  }
}
于 2012-10-15T10:06:30.010 回答
5

Cats有一个称为foldM的方法,它可以进行短路(for Vector, List, Stream, ...)。

它的工作原理如下:

def sumEvenNumbers(nums: Stream[Int]): Option[Long] = {
  import cats.implicits._
  nums.foldM(0L) {
    case (acc, c) if c % 2 == 0 => Some(acc + c)
    case _ => None
  }
}

如果它找到一个非偶数元素,则返回None而不计算其余元素,否则它返回偶数条目的总和。

如果要保持计数直到找到偶数条目,则应使用Either[Long, Long]

于 2018-10-12T13:58:09.330 回答
5

您可以foldM从猫库中使用(如@Didac 建议的那样),但如果您想获得实际总和,我建议使用Either而不是。Option

bifoldMap用于从中提取结果Either

import cats.implicits._

def sumEven(nums: Stream[Int]): Either[Int, Int] = {
    nums.foldM(0) {
      case (acc, n) if n % 2 == 0 => Either.right(acc + n)
      case (acc, n) => {
        println(s"Stopping on number: $n")
        Either.left(acc)
      }
    }
  }

例子:

println("Result: " + sumEven(Stream(2, 2, 3, 11)).bifoldMap(identity, identity))
> Stopping on number: 3
> Result: 4

println("Result: " + sumEven(Stream(2, 7, 2, 3)).bifoldMap(identity, identity))
> Stopping on number: 7
> Result: 2
于 2020-04-13T12:05:38.807 回答
1

@Rex Kerr 你的回答帮助了我,但我需要调整它以使用 Either

  
  def foldOrFail[A,B,C,D](map: B => Either[D, C])(merge: (A, C) => A)(initial: A)(it: Iterable[B]): [D, A] = {
    val ii= it.iterator
    var b=初始
    而(ii.hasNext){
      val x= ii.next
      地图(x)匹配{
        case Left(error) => return Left(error)
        案例右(d)=> b=合并(b,d)
      }
    }
    右(b)
  }
于 2013-03-22T20:50:48.960 回答
1

您可以尝试使用临时变量并使用 takeWhile。这是一个版本。

  var continue = true

  // sample stream of 2's and then a stream of 3's.

  val evenSum = (Stream.fill(10)(2) ++ Stream.fill(10)(3)).takeWhile(_ => continue)
    .foldLeft(Option[Int](0)){

    case (result,i) if i%2 != 0 =>
          continue = false;
          // return whatever is appropriate either the accumulated sum or None.
          result
    case (optionSum,i) => optionSum.map( _ + i)

  }

在这种情况下evenSum应该是Some(20)

于 2015-08-04T18:32:58.520 回答
0

一个更漂亮的解决方案是使用跨度:

val (l, r) = numbers.span(_ % 2 == 0)
if(r.isEmpty) Some(l.sum)
else None

...但如果所有数字都是偶数,它会遍历列表两次

于 2012-10-15T09:32:39.010 回答
0

您可以在遇到终止条件时抛出一个精心挑选的异常,并在调用代码中处理它。

于 2012-10-15T09:35:14.800 回答
0

只是出于“学术”原因(:

var headers = Source.fromFile(file).getLines().next().split(",")
var closeHeaderIdx = headers.takeWhile { s => !"Close".equals(s) }.foldLeft(0)((i, S) => i+1)

需要两次,但它是一个很好的衬里。如果未找到“关闭”,它将返回

headers.size

另一个(更好)是这个:

var headers = Source.fromFile(file).getLines().next().split(",").toList
var closeHeaderIdx = headers.indexOf("Close")
于 2015-06-13T17:32:20.067 回答