web-dev-qa-db-ja.com

フォールドの早い段階で中止

フォールドを早期に終了する最良の方法は何ですか?簡単な例として、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を実行した場合のように、はるかにきれいで明確になります)そして最悪なのは、偶数でない数に遭遇した場合でもiterable全体をトラバースすることです。

それでは、このような折り畳みを書く最良の方法は何でしょうか?私はこれを再帰的に書くだけですか、それとももっと受け入れられる方法がありますか?

79
Heptic

私の最初の選択は通常、再帰を使用することです。適度にコンパクトであるだけで、潜在的に高速(確実に低速ではない)であり、早期終了ではロジックがより明確になります。この場合、少し厄介なネストされた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)
}

私の2番目の選択肢は、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をどこにでも配置できることに注意してください。

次に、foldを再実装して(自分自身で、またはそれを行うライブラリを見つけることで)、早期終了を通知できるようにします。これを行う2つの自然な方法は、値を伝播するのではなく、値を含むOptionを伝播することです。ここで、Noneは終了を示します。または、完了を通知する2番目のインジケータ関数を使用します。 Kim Stebelが示したScalazレイジーフォールドは、最初のケースを既にカバーしているので、2番目のケースを(可変実装で)示します。

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自体はうまく機能しますが、そうではなく、それを手作業で行うのに余分な作業が必要になるため、ここで使用するのはばかげたオプションになります。)

60
Rex Kerr

あなたが説明するシナリオ(いくつかの望ましくない条件で終了する)は、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
23
Dylan

ScalazのfoldRightの遅延バージョンを使用して、機能的なスタイルで必要なことを実行できます。より詳細な説明については、 このブログ投稿 を参照してください。このソリューションではStreamを使用しますが、iterable.toStreamを使用すると、IterableStreamに効率的に変換できます。

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

これは、匿名関数が2回しか呼び出されないことを明確に示しています(つまり、奇数に遭遇するまで)。これは、その署名(Streamの場合)がdef foldr[B](b: B)(f: (Int, => B) => B)(implicit r: scalaz.Foldable[Stream]): Bであるfoldrの定義によるものです。無名関数は2番目の引数として名前によるパラメーターを受け取るため、評価する必要はありません。

ところで、あなたはまだOPのパターンマッチングソリューションでこれを書くことができますが、私はif/elseとマップをよりエレガントに見つけます。

14
Kim Stebel

まあ、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
  }
}
6
missingfaktor

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

コレクションの要素の1つが均等ではなくなるとすぐに戻ります。

4
Didac Montero

一時変数と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)でなければなりません。

1
seagull1089

@Rex Kerrあなたの答えは私を助けましたが、私はどちらかを使用するためにそれを微調整する必要がありました

  
 def foldOrFail [A、B、C、D](map:B => Both [D、C])(merge:(A、C)=> A)(initial:A)(it:Iterable [B]):どちらか[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)
} 
1
Core

より美しい解決策は、スパンを使用することです:

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

...しかし、すべての数値が偶数の場合、リストを2回走査します

0
Arjan

「アカデミック」な理由のため(:

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

2回かかるが、それはする必要がありますが、それは素敵な1つのライナーです。 「閉じる」が見つからない場合は、戻ります

headers.size

別の(より良い)これは:

var headers = Source.fromFile(file).getLines().next().split(",").toList
var closeHeaderIdx = headers.indexOf("Close")
0
ozma

終了基準に遭遇すると、適切に選択された例外をスローして、呼び出しコードで処理できます。

0
waldrumpus