フォールドを早期に終了する最良の方法は何ですか?簡単な例として、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全体をトラバースすることです。
それでは、このような折り畳みを書く最良の方法は何でしょうか?私はこれを再帰的に書くだけですか、それとももっと受け入れられる方法がありますか?
私の最初の選択は通常、再帰を使用することです。適度にコンパクトであるだけで、潜在的に高速(確実に低速ではない)であり、早期終了ではロジックがより明確になります。この場合、少し厄介なネストされた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
自体はうまく機能しますが、そうではなく、それを手作業で行うのに余分な作業が必要になるため、ここで使用するのはばかげたオプションになります。)
あなたが説明するシナリオ(いくつかの望ましくない条件で終了する)は、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
ScalazのfoldRightの遅延バージョンを使用して、機能的なスタイルで必要なことを実行できます。より詳細な説明については、 このブログ投稿 を参照してください。このソリューションではStream
を使用しますが、iterable.toStream
を使用すると、Iterable
をStream
に効率的に変換できます。
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とマップをよりエレガントに見つけます。
まあ、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
}
}
一時変数と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)
でなければなりません。
@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) }
より美しい解決策は、スパンを使用することです:
val (l, r) = numbers.span(_ % 2 == 0)
if(r.isEmpty) Some(l.sum)
else None
...しかし、すべての数値が偶数の場合、リストを2回走査します
「アカデミック」な理由のため(:
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")
終了基準に遭遇すると、適切に選択された例外をスローして、呼び出しコードで処理できます。