在折叠中早期中止

时间:2012-10-15 09:24:59

标签: scala functional-programming

早期终止折叠的最佳方法是什么?作为一个简化的例子,想象一下我想总结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和返回 - 它会更清晰和更清晰),最糟糕的是,它遍历整个迭代,即使遇到非 - 甚至数字。

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

11 个答案:

答案 0 :(得分:57)

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

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,因为它保持其他所有内容完整无缺,你只需将折叠包裹在def中,这样你就可以从中返回 - 在这种情况下,你已经有了一个方法,所以:

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

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

如果我经常这样做并希望它在某个方法的中间位置(所以我不能只使用return),我可能会使用异常处理来生成非本地控制流。毕竟,它擅长的是什么,错误处理并不是它唯一有用的时间。唯一的技巧是避免生成堆栈跟踪(这实际上很慢),这很容易,因为特征NoStackTrace及其子特征ControlThrowable已经为您做了。 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 lazy fold已经涵盖了第一种情况,因此我将展示第二种情况(具有可变实现):

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本身就可以正常工作,但它没有,并且手工完成这项工作所需的额外工作使得在这里使用它是一个愚蠢的选择。)

答案 1 :(得分:21)

您描述的场景(在某些不需要的情况下退出)似乎是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)

这也适用于Iterator s / Iterable s。我建议你使用“偶数总和,然后打破奇数”的解决方案是:

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

答案 2 :(得分:14)

您可以使用scalaz中的懒惰版本的foldRight在功能样式中执行您想要的操作。有关更深入的说明,请参阅this blog post。虽然此解决方案使用Stream,但您可以使用Iterable有效地将Stream转换为iterable.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。请注意,匿名函数将by name参数作为其第二个参数,因此无需进行评估。

不过,你仍然可以用OP的模式匹配解决方案来编写这个,但我发现if / else和map更优雅。

答案 3 :(得分:6)

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

答案 4 :(得分:4)

您可以从cats lib中使用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

答案 5 :(得分:2)

Cats具有一种称为foldM的方法,该方法会短路(对于VectorListStream,...)。

它的工作原理如下:

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

Stream上的元素之一不均匀时,它就会返回。

答案 6 :(得分:1)

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

  
  def foldOrFail[A,B,C,D](map: B => Either[D, C])(merge: (A, C) => A)(initial: A)(it: Iterable[B]): Either[D, A] = {
    val ii= it.iterator
    var b= initial
    while (ii.hasNext) {
      val x= ii.next
      map(x) match {
        case Left(error) => return Left(error)
        case Right(d) => b= merge(b, d)
      }
    }
    Right(b)
  }

答案 7 :(得分:1)

您可以尝试使用临时var并使用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)

答案 8 :(得分:0)

更美观的解决方案是使用span:

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

...但如果所有数字都是

,它会遍历列表两次

答案 9 :(得分:0)

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

答案 10 :(得分:0)

仅限于#34;学术"原因(:

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