弃权弃牌


88

尽早终止折页的最佳方法是什么?作为一个简化的示例,假设我想对中的数字求和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,则它会更加清晰明了),最糟糕的是,即使遇到一个非偶数,它也会遍历整个可迭代项。

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


您是否要终止并记录中间答案?
布赖恩·阿格纽

在这种情况下,不会。但是在稍微更一般的情况下,我可能想返回一个有错误或任何东西的Either
Heptic 2012年


关于中断循环的答复也可能有用:stackoverflow.com/a/2742941/1307721
ejoubaud 2015年

Answers:


64

我的首选通常是使用递归。它的压缩程度仅适度地降低,可能更快(肯定不会更慢),并且在提前终止时可以使逻辑更清晰。在这种情况下,您需要嵌套的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,因为它可以使其他所有内容保持不变,并且您只需要将折叠包裹在a中,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在任何地方,而不仅仅是包装整个方法。

对我而言,下一步是重新实现fold(我自己或寻找一个实现该功能的库),以便它可以表示提前终止。这样做的两种自然方法是不传播值,而是传播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,它本身会很​​好地工作,但如果没有的话,那么手工完成这项工作会花费很多额外的工作,因此在这里使用是一种愚蠢的选择。)


foldOrFail正是我在思考问题时想到的。当所有这些都很好地封装后,没有理由在实现IMO中不使用可变的迭代器和while循环。使用iterator以及递归没有意义。
2012年

@Rex Kerr,感谢您的回答,我调整了一个使用Either的供自己使用的版本...(我将其发布为答案)
2013年

基于收益的解决方案的缺点之一可能是,花一点时间才能意识到它适用于哪个函数:sumEvenNumbers或foldop
Ivan Balashov 2015年

1
@IvanBalashov -嗯,这需要一段时间,一旦学会什么Scala的规则是return(即,从最里面的明确方法返回你发现它),但之后,它应该不是很长的时间。规则很明确,并且def给出了封闭方法的位置。
Rex Kerr 2015年

我喜欢你的foldOrFail但我个人会作出的返回类型B不是Option[B]因为那时它像倍,其中返回类型是一样的零蓄能器的类型。只需用b替换所有Option返回。且pas在None中为零。毕竟,这个问题想要一个可以尽早终止而不是失败的折线。
卡尔

26

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

这正是我一直在寻找的简单性-谢谢!
坦纳

14

您可以使用scalaz中的懒版foldRight以功能样式执行所需的操作。有关更深入的说明,请参阅此博客文章。虽然这种解决方案采用的是Stream,你可以转换一个IterableStream与高效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 by参数作为其第二个参数,因此无需对其进行求值。

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


如果你把会发生什么事println之前if-else体现在哪里?
missingfaktor 2012年

@missingfaktor:然后打印0和1,但不打印更多
Kim Stebel,2012年

@missingfaktor:因为我的观点更容易理解,所以我在答案中做了更改
Kim Stebel,2012年

1
请注意,您可以使用将任何可迭代的对象转换为流toStream,因此该答案比起初看起来更通用。
Rex Kerr 2012年

2
由于您使用scalaz,为什么不使用‛0.some‛?
pedrofurla 2012年

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

2
而不是Some(0): Option[Int]你可以只写Option(0)
路易吉·普林格

1
@LuigiPlinge,是的。我只是复制粘贴了OP的代码,只做了必要的修改就可以了。
missingfaktor 2012年

5

有一个方法叫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
  }
}

一旦集合中的元素之一不相等,它就会返回。


4

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

来这里发布类似的答案,因为我认为这是最便捷但仍是FP的方法。我感到惊讶的是没有人为此投票。因此,抓住我的+1。(我更愿意(acc + n).asRight而不是,Either.right(acc + n)但无论如何)
凌晨

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
      map(x)匹配{
        case左(错误)=>返回左(错误)
        大小写Right(d)=> b = merge(b,d)
      }
    }
    对(b)
  }

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)

  }

evenSumSome(20)在这种情况下。



0

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

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

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


2
我喜欢您的解决方案所举例说明的横向思维,但它只能解决问题中所选择的具体示例,而不是解决如何尽早终止折页的一般问题。
iainmcgin 2012年

我想展示如何进行反向操作,而不是提前终止折叠,而是仅折叠(在这种情况下为和)超过我们要折叠的值
Arjan 2012年

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")
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.