La mia prima scelta di solito sarebbe usare la ricorsione. È solo moderatamente meno compatto, è potenzialmente più veloce (certamente non più lento) e nella terminazione anticipata può rendere la logica più chiara. In questo caso hai bisogno di def annidati che è un po 'imbarazzante:
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)
}
La mia seconda scelta sarebbe quella di usare return
, poiché mantiene intatto tutto il resto e devi solo avvolgere la piega in def
modo da avere qualcosa da cui tornare - in questo caso, hai già un metodo, quindi:
def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
Some(nums.foldLeft(0){ (n,x) =>
if ((n % 2) != 0) return None
n+x
})
}
che in questo caso particolare è molto più compatto della ricorsione (anche se siamo stati particolarmente sfortunati con la ricorsione poiché dovevamo fare una trasformazione iterabile / iteratore). Il flusso di controllo instabile è qualcosa da evitare quando tutto il resto è uguale, ma qui non lo è. Nessun danno nell'usarlo nei casi in cui è prezioso.
Se lo facessi spesso e lo volessi all'interno di un metodo da qualche parte (quindi non potrei usare solo return), probabilmente userei la gestione delle eccezioni per generare un flusso di controllo non locale. Questo è, dopo tutto, ciò in cui è bravo e la gestione degli errori non è l'unica volta in cui è utile. L'unico trucco è evitare di generare una traccia dello stack (che è molto lenta), ed è facile perché il tratto NoStackTrace
e il suo tratto figlio lo ControlThrowable
fanno già per te. Scala lo utilizza già internamente (infatti, è così che implementa il ritorno dall'interno della piega!). Facciamo il nostro (non può essere annidato, anche se si potrebbe risolverlo):
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
})
}
Qui ovviamente return
è meglio usare , ma tieni presente che potresti mettere shortcut
ovunque, non solo avvolgere un intero metodo.
Il prossimo in linea per me sarebbe reimplementare fold (io stesso o trovare una libreria che lo faccia) in modo che possa segnalare una terminazione anticipata. I due modi naturali per farlo sono non propagare il valore ma un Option
contenente il valore, dove None
significa terminazione; o per utilizzare una seconda funzione di indicatore che segnala il completamento. Il lazy fold di Scalaz mostrato da Kim Stebel copre già il primo caso, quindi mostrerò il secondo (con un'implementazione mutabile):
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)(_ + _)
(Dipende da te se implementare la terminazione per ricorsione, ritorno, pigrizia, ecc.)
Penso che copra le principali varianti ragionevoli; ci sono anche altre opzioni, ma non sono sicuro del motivo per cui si dovrebbero usarle in questo caso. (di per Iterator
sé funzionerebbe bene se avesse un findOrPrevious
, ma non lo fa, e il lavoro extra necessario per farlo a mano lo rende un'opzione sciocca da usare qui.)