Moim pierwszym wyborem byłoby zwykle użycie rekurencji. Jest tylko umiarkowanie mniej zwarty, jest potencjalnie szybszy (z pewnością nie wolniejszy), a we wczesnym zakończeniu może uczynić logikę bardziej przejrzystą. W tym przypadku potrzebujesz zagnieżdżonych definicji, co jest trochę niezręczne:
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)
}
Moim drugim wyborem byłoby użycie return
, ponieważ zachowuje wszystko inne nienaruszone i wystarczy zawinąć fałdę, def
aby mieć z czego wrócić - w tym przypadku masz już metodę, więc:
def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
Some(nums.foldLeft(0){ (n,x) =>
if ((n % 2) != 0) return None
n+x
})
}
który w tym konkretnym przypadku jest dużo bardziej zwarty niż rekurencja (chociaż mieliśmy szczególnego pecha z rekurencją, ponieważ musieliśmy wykonać iterowalną transformację iteracyjną). Skokowy przepływ sterowania jest czymś, czego należy unikać, gdy wszystko inne jest równe, ale tutaj tak nie jest. Nie szkodzi używaniu go w przypadkach, gdy jest cenny.
Gdybym robił to często i chciałbym, żeby było to gdzieś w środku metody (więc nie mogłem po prostu użyć powrotu), prawdopodobnie użyłbym obsługi wyjątków do wygenerowania nielokalnego przepływu sterowania. To w końcu jest dobry, a obsługa błędów nie jest jedynym, kiedy jest przydatna. Jedyną sztuczką jest unikanie generowania śladu stosu (który jest naprawdę powolny), a jest to łatwe, ponieważ cecha NoStackTrace
i jej cecha potomna ControlThrowable
już to robią. Scala już używa tego wewnętrznie (w rzeczywistości tak implementuje powrót z wnętrza zagięcia!). Zróbmy własne (nie można zagnieżdżać, chociaż można to naprawić):
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
})
}
Tutaj oczywiście użycie return
jest lepsze, ale pamiętaj, że możesz umieścić w shortcut
dowolnym miejscu, a nie tylko opakować całą metodę.
Następnym krokiem byłoby ponowne zaimplementowanie spasowania (samodzielnie lub znalezienie biblioteki, która to robi), aby mogło sygnalizować wczesne zakończenie. Dwa naturalne sposoby na zrobienie tego to nie propagowanie wartości, ale Option
zawarcie wartości, gdzie None
oznacza zakończenie; lub użyć drugiej funkcji wskaźnika, która sygnalizuje zakończenie. Leniwe zwinięcie Scalaz pokazane przez Kim Stebel obejmuje już pierwszy przypadek, więc pokażę drugi (ze zmienną implementacją):
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)(_ + _)
(To, czy zaimplementujesz zakończenie przez rekursję, powrót, lenistwo itp., Zależy od Ciebie).
Myślę, że obejmuje to główne rozsądne warianty; jest też kilka innych opcji, ale nie jestem pewien, po co ich używać w tym przypadku. ( Iterator
Samo działałoby dobrze, gdyby miało findOrPrevious
, ale tak nie jest, a dodatkowa praca, jaką trzeba wykonać ręcznie, sprawia, że jest to głupia opcja w tym miejscu).