Erken iptal


90

Katlamayı erken bitirmenin en iyi yolu nedir? Basitleştirilmiş bir örnek olarak, sayıları bir içinde özetlemek istediğimi hayal edin Iterable, ancak beklemediğim bir şeyle karşılaşırsam (tek bir sayı söyleyin) bitirmek isteyebilirim. Bu bir ilk yaklaşımdır

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

Bununla birlikte, bu çözüm oldukça çirkin (olduğu gibi, her biri bir. .

Öyleyse, erken sona eren böyle bir bölüm yazmanın en iyi yolu nedir? Gidip bunu yinelemeli olarak mı yazmalıyım yoksa daha kabul gören bir yol var mı?


Ara cevabı sonlandırmak ve kaydetmek istiyor musunuz?
Brian Agnew

Bu durumda hayır. Ancak biraz daha genel bir durumda, hata ya da başka bir şey içeren bir Ya döndürmek isteyebilirim
Heptic


Döngülerin kırılmasıyla ilgili bu yanıt da yararlı olabilir: stackoverflow.com/a/2742941/1307721
ejoubaud

Yanıtlar:


65

İlk tercihim genellikle özyinelemeyi kullanmak olacaktır. Yalnızca orta derecede daha az kompakttır, potansiyel olarak daha hızlıdır (kesinlikle daha yavaş değildir) ve erken sonlandırmada mantığı daha net hale getirebilir. Bu durumda, biraz garip olan iç içe geçmiş tanımlara ihtiyacınız vardır:

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

İkinci seçimim return, diğer her şeyi olduğu gibi tuttuğu için kullanmak olacaktır ve tek defyapmanız gereken, geri dönecek bir şeyiniz olması için katlamayı sarmaktır - bu durumda, zaten bir yönteminiz var, yani:

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

ki bu özel durumda özyinelemeden çok daha kompakttır (yinelenebilir / yineleyici bir dönüşüm yapmak zorunda olduğumuz için özellikle özyineleme ile şanssız kaldık). Sarsıntılı kontrol akışı, her şey eşit olduğunda kaçınılması gereken bir şeydir, ama burada değil. Değerli olduğu durumlarda kullanmaktan zarar gelmez.

Bunu sık sık yapıyor olsaydım ve bir yerde bir yöntemin ortasında isteseydim (bu yüzden sadece dönüşü kullanamazdım), muhtemelen yerel olmayan kontrol akışı oluşturmak için istisna işlemeyi kullanırdım. Sonuçta, iyi olduğu şeydir ve hata işleme, yararlı olduğu tek zaman değildir. Tek numara bir yığın izleme oluşturmaktan kaçınmaktır (ki bu gerçekten yavaştır) ve bu kolaydır çünkü özellik NoStackTraceve onun çocuk özelliği ControlThrowablezaten bunu sizin için yapıyor. Scala bunu zaten dahili olarak kullanıyor (aslında, katın içinden dönüşü böyle gerçekleştiriyor!). Hadi kendimiz yapalım (iç içe geçirilemez, ancak biri bunu düzeltebilir):

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

Burada kullanmak elbette returndaha iyidir, ancak shortcutsadece tüm bir yöntemi sarmakla kalmayıp, istediğiniz yere koyabileceğinizi unutmayın .

Benim için sonraki sırada, katlamayı yeniden uygulamak (ya kendim olsun ya da bunu yapan bir kitaplık bulmak), böylece erken sonlandırma sinyali verebilsin. Bunu yapmanın iki doğal yolu, değeri yaymak değil Option, değeri içermektir, burada Nonefesih anlamına gelir; veya tamamlandığını gösteren ikinci bir gösterge işlevi kullanmak için. Kim Stebel tarafından gösterilen Scalaz tembel katlanma zaten ilk vakayı kapsıyor, bu yüzden ikinciyi göstereceğim (değiştirilebilir bir uygulama ile):

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)(_ + _)

(Sonlandırmayı yineleme, dönüş, tembellik vb. İle uygulayıp uygulamayacağınız size bağlıdır.)

Sanırım bu ana makul değişkenleri kapsıyor; başka seçenekler de var, ancak bu durumda neden birinin bunları kullanacağından emin değilim. ( IteratorA'ya sahip olsaydı kendisi iyi çalışırdı findOrPrevious, ama olmadı ve bunu elle yapmak için gereken ekstra çalışma onu burada kullanmak için aptalca bir seçenek haline getiriyor.)


foldOrFailBen soru hakkında düşünürken ile gelmişti tam olarak ne olduğunu. Hepsi güzel bir şekilde kapsüllendiğinde, IMO uygulamasında değiştirilebilir bir yineleyici ve bir while döngüsü kullanmamak için bir neden yok. iteratorÖzyineleme ile birlikte kullanmak mantıklı değil.
0__

@Rex Kerr, cevabınız için teşekkürler Her ikisini de kullanan kendi kullanımım için bir versiyonda değişiklik yaptım ... (Cevap olarak göndereceğim)
Core

Muhtemelen getiri temelli çözümün dezavantajlarından biri, hangi işlevin hangi işlev için geçerli olduğunu anlamanın biraz zaman almasıdır: sumEvenNumbersveya katlamaop
Ivan Balashov

1
@IvanBalashov - Eh, biraz zaman alıyor kez Scala'nın kuralları için ne olduğunu öğrenmek için return(yani, bu açık yöntemle içteki dönene sen bulabilirsiniz), ama bundan sonra çok uzun sürmez gerektiğini. Kural oldukça açık ve defçevreleme yönteminin nerede olduğunu ortaya koyuyor .
Rex Kerr

1
Ben senin foldOrFail gibi ama şahsen ben dönüş türü yapmış olur Bdeğil Option[B]döndürme türü sıfır akümülatörlerde türüyle aynı olduğu o zaman kat gibi davranır çünkü. Basitçe tüm Seçenek dönüşlerini b ile değiştirin. ve sıfır olarak Yok'ta pas. Sonuçta soru, başarısız olmaktan ziyade erken sonlanabilecek bir katlanma istedi.
Karl

26

Tanımladığınız senaryo (bazı istenmeyen koşullardan çıkış), takeWhileyöntem için iyi bir kullanım durumu gibi görünüyor . Esasen öyledir filter, ancak koşulu karşılamayan bir unsurla karşılaşıldığında sona ermelidir.

Örneğin:

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)

Bu da Iterators / Iterables için gayet iyi çalışacak . "Çift sayıların toplamı, ancak tek üzerinde kırılma" için önerdiğim çözüm şudur:

list.iterator.takeWhile(_ % 2 == 0).foldLeft(...)

Ve sadece tek bir sayıya ulaştığında zamanınızı boşa harcamadığını kanıtlamak için ...

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

Bu tam olarak aradığım türden bir basitlikti - teşekkür ederim!
Tanner

14

Scalaz'da foldRight'ın tembel versiyonunu kullanarak fonksiyonel bir tarzda istediğinizi yapabilirsiniz. Daha ayrıntılı bir açıklama için bu blog gönderisine bakın . Bu çözüm bir kullanır iken Stream, bir dönüştürebilirsiniz Iterablebir içine Streamverimli bir şekilde 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
})

Bu sadece yazdırır

0
1

bu, anonim işlevin yalnızca iki kez çağrıldığını açıkça gösterir (yani, tek sayı ile karşılaşana kadar). Bu, imzası (olması durumunda Stream) olan foldr'ın tanımından kaynaklanmaktadır def foldr[B](b: B)(f: (Int, => B) => B)(implicit r: scalaz.Foldable[Stream]): B. Anonim işlevin ikinci bağımsız değişken olarak bir ada göre parametre aldığını, bu nedenle değerlendirilmesine gerek olmadığını unutmayın.

Btw, bunu yine de OP'nin örüntü eşleştirme çözümüyle yazabilirsiniz, ancak if / else buluyorum ve haritayı daha zarif buluyorum.


Daha printlnönce koyarsanız ne olur if- elseifade?
missingfaktor

@missingfaktor: sonra 0 ve 1'i yazdırır, ancak daha fazlasını değil
Kim Stebel

@missingfaktor: Demek istediğim bu şekilde yapmak daha kolay olduğundan,
cevabımda

1
İle herhangi bir yinelenebilirliği bir akışa dönüştürebileceğinizi unutmayın toStream, bu nedenle bu yanıt ilk bakışta göründüğünden daha genel amaçlıdır.
Rex Kerr

2
Scalaz kullandığına göre neden ‛0.some‛ kullanmıyorsun?
pedrofurla

7

Scala yerel olmayan iadelere izin veriyor. Bunun iyi bir tarz olup olmadığı konusunda farklı görüşler var.

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)

DÜZENLE:

Bu özel durumda, @Arjan'ın önerdiği gibi, şunları da yapabilirsiniz:

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]senin yerine sadece yazabilirsin Option(0).
Luigi Plinge

1
@LuigiPlinge, evet. Sadece OP'nin kodunu kopyalayıp yapıştırdım ve sadece bir noktaya değinmeye yetecek kadar değişiklik yaptım.
missingfaktor

5

Kediler bir yöntem denilen sahiptir foldM kısa devre yok (için Vector, List, Stream, ...).

Aşağıdaki gibi çalışır:

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

Eşit olmayan bir öğe bulursa None, geri kalanını hesaplamadan döndürür , aksi takdirde çift girdilerin toplamını döndürür.

Çift bir giriş bulunana kadar saymayı sürdürmek istiyorsanız, bir Either[Long, Long]


4

foldMCats kitaplığından kullanabilirsiniz (@Didac tarafından önerildiği gibi), ancak gerçek toplamı almak istiyorsanız Eitherbunun yerine kullanmanızı öneririm Option.

bifoldMapsonucu çıkarmak için kullanılır 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)
      }
    }
  }

örnekler:

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

Buraya benzer bir cevap göndermek için geldim, çünkü bu benim görüşüme göre yapmanın en uygun ve hala FP yolu. Kimsenin buna oy vermemesine şaşırdım. O halde + 1'imi alın. (Tercihim (acc + n).asRightyerine Either.right(acc + n)yine ama)
abdolence

yerine bifoldMapsadece fold(L => C, R => C): Cüzerinde çalışacak Either[L, R]ve sonra bir ihtiyacım yokMonoid[C]
Ben Hutchison

1

@Rex Kerr cevabın bana yardımcı oldu, ama ikisini de kullanmak için ince ayar yapmam gerekiyordu

  
  def foldOrFail [A, B, C, D] (harita: B => İkisinden biri [D, C]) (birleştirme: (A, C) => A) (başlangıç: A) (it: Yinelenebilir [B]): Ya [D, A] = {
    val ii = it.iterator
    var b = ilk
    while (ii.hasNext) {
      val x = ii.sonraki
      harita (x) eşleşmesi {
        case Left (hata) => Sola dön (hata)
        case Sağ (d) => b = birleştirme (b, d)
      }
    }
    Sağ (b)
  }

1

Geçici bir var kullanmayı ve takeWhile kullanmayı deneyebilirsiniz. İşte bir versiyon.

  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)

  }

evenSumOlmalı Some(20)bu durumda.



0

Daha güzel bir çözüm, span kullanmak olabilir:

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

... ancak tüm sayılar çift ise listeyi iki kez geçiyor


2
Çözümünüzün örneklediği yanal düşünmeyi seviyorum, ancak bu, bir katlamanın nasıl erken sonlandırılacağına dair genel soruyla uğraşmak yerine, yalnızca soruda seçilen belirli örneği çözer.
iainmcgin

Tersini nasıl yapacağımı göstermek istedim, bir katlamayı erken bitirmek değil, sadece katlamak istediğimiz değerlerin üzerine katlamak (bu durumda toplamı)
Arjan

0

Yalnızca "akademik" nedenlerle (:

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

Daha sonra iki kez alır ama güzel bir astar. "Kapat" bulunamazsa geri dönecektir

headers.size

Bir diğeri (daha iyi) şudur:

var headers = Source.fromFile(file).getLines().next().split(",").toList
var closeHeaderIdx = headers.indexOf("Close")
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.