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


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を実行した場合のように、はるかにクリーンで明確になります)、最悪の場合、偶数以外の数値に遭遇した場合でも、反復可能全体をトラバースします。 。

では、このようなフォールドを作成するための最良の方法は何でしょうか。これを再帰的に記述する必要がありますか、それとももっと受け入れられる方法がありますか?


中間回答を終了して記録しますか?
ブライアンアグニュー2012年

この場合、いいえ。しかし、もう少し一般的なケースでは、エラーか何かがあるどちらかを返したいと思うかもしれません
Heptic


ループの解除に関するこの返信も役立つ場合があります:stackoverflow.com/a/2742941/1307721
ejoubaud 2015年

回答:


64

私の最初の選択は通常、再帰を使用することです。それは適度にコンパクトではなく、潜在的に高速であり(確かに低速ではありません)、早期終了でロジックをより明確にすることができます。この場合、ネストされた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ません。手作業でそれを行うために余分な作業が必要になるため、ここで使用するのはばかげたオプションになります。)


それfoldOrFailはまさに私が質問について考えたときに思いついたものです。すべてが適切にカプセル化されている場合、実装IMOで可変イテレーターとwhileループを使用しない理由はありません。iterator再帰と一緒に使用することは意味がありません。
0__ 2012年

@Rex Kerr、あなたの答えに感謝します私はどちらかを使用する私自身の使用のためにバージョンを微調整しました...(私は答えとしてそれを投稿するつもりです)
コア

おそらくリターンベースのソリューションの短所の1つは、それがどの機能に適用されるsumEvenNumbersかを理解するのに時間がかかることです:またはフォールドop
Ivan Balashov 2015

1
@IvanBalashov -まあ、それは少し時間がかかりたら、 Scalaのルールがために何であるかを学ぶためにreturn(すなわち、それはあなたがそれを見つける最も内側の明示的なメソッドから返されます)が、それの後に、それは非常に長い時間はかからないはず。ルールはかなり明確でdefあり、囲みメソッドがどこにあるかを示します。
レックスカー

私はあなたのfoldOrFailが好きですが、個人的には戻り値の型を作成したBはずです。Option[B]なぜなら、戻り値の型がゼロアキュムレータの型と同じである場合、それはfoldのように動作するからではありません。すべてのOptionリターンをbに置き換えるだけです。ゼロとしてNoneのpas。結局のところ、質問は失敗するのではなく、早期に終了できるフォールドを望んでいました。
カール

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ますが、を使用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

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

ところで、OPのパターンマッチングソリューションを使用してこれを書くことはできますが、if / elseとマップの方がエレガントだと思います。


println前にif-else式を置くとどうなりますか?
missingfaktor

@missingfaktor:その後、0と1を出力しますが、それ以上は出力しません
Kim

@missingfaktor:私の主張はこのようにするほうが簡単なので、答えを変更しました
Kim

1
を使用するとtoStream、任意の反復可能オブジェクトをストリームに変換できるため、この回答は最初に表示されるよりも汎用的であることに注意してください。
レックスカー

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)
Luigi Plinge 2012年

1
@LuigiPlinge、はい。OPのコードをコピーして貼り付けただけで、要点を説明するのに必要な変更のみを行いました。
missingfaktor

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

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


4

foldMcats libから使用できます(@Didacによって提案されています)が、実際の合計を取得したい場合EitherOption、代わりに使用することをお勧めします。

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)
禁欲

で動作するbifoldMapだけでなく、必要ありませんfold(L => C, R => C): CEither[L, R]Monoid[C]
ベンハッチソン

1

@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 =初期
    while(ii.hasNext){
      val x = ii.next
      map(x)match {
        case Left(error)=> return Left(error)
        case Right(d)=> b = merge(b、d)
      }
    }
    右(b)
  }

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)、この場合に。



0

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

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

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


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)

2回かかるはずですが、それは素晴らしいワンライナーです。「閉じる」が見つからない場合は戻ります

headers.size

もう1つ(より良い)はこれです:

var headers = Source.fromFile(file).getLines().next().split(",").toList
var closeHeaderIdx = headers.indexOf("Close")
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.