Interrompi presto in una piega


88

Qual è il modo migliore per terminare anticipatamente un fold? Come esempio semplificato, immagina di voler riassumere i numeri in un Iterable, ma se incontro qualcosa che non mi aspetto (ad esempio un numero dispari) potrei voler terminare. Questa è una prima approssimazione

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

Tuttavia, questa soluzione è piuttosto brutta (come in, se facessi un .foreach e un ritorno - sarebbe molto più pulito e chiaro) e peggio di tutto, attraversa l'intero iterabile anche se incontra un numero non pari .

Quindi quale sarebbe il modo migliore per scrivere una piega come questa, che termina presto? Dovrei semplicemente scrivere questo in modo ricorsivo o c'è un modo più accettato?


Vuoi terminare e registrare la risposta intermedia?
Brian Agnew

In questo caso no. Ma in un caso leggermente più generale potrei voler restituire un O che ha un errore o qualcosa del genere
Heptic


Questa risposta di rompere fuori di loop potrebbe anche essere trovato utili: stackoverflow.com/a/2742941/1307721
ejoubaud

Risposte:


64

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 defmodo 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 NoStackTracee il suo tratto figlio lo ControlThrowablefanno 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 shortcutovunque, 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 Optioncontenente il valore, dove Nonesignifica 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 Iteratorsé 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.)


Il foldOrFailè esattamente quello che avevo messo a punto quando si parla della questione. Nessun motivo per non utilizzare un iteratore mutabile e un ciclo while nell'implementazione IMO, quando tutto è ben incapsulato. L'uso iteratorinsieme alla ricorsione non ha senso.
0__

@Rex Kerr, grazie per la tua risposta, ho modificato una versione per uso personale che utilizza Either ... (la posterò come risposta)
Core

Probabilmente uno degli svantaggi della soluzione basata sul ritorno è che ci vuole un po 'per capire a quale funzione si applica: sumEvenNumberso fold'sop
Ivan Balashov

1
@IvanBalashov - Beh, ci vuole un po , una volta per imparare ciò che le regole di Scala sono per return(cioè, torna dal più interno metodo esplicito lo trovate in), ma dopo che non dovrebbe prendere molto tempo. La regola è abbastanza chiara e il defmetodo di inclusione è rivelato.
Rex Kerr

1
Mi piace il tuo foldOrFail ma personalmente avrei fatto il tipo di ritorno Bnon Option[B]perché allora si comporta come un piega dove il tipo di ritorno è lo stesso del tipo di accumulatore zero. Sostituisci semplicemente tutti i ritorni delle Opzioni con b. e pas in Nessuno come zero. Dopo tutto la domanda voleva una piega che potesse terminare presto, piuttosto che fallire.
Karl

26

Lo scenario che descrivi (uscita da una condizione indesiderata) sembra un buon caso d'uso per il takeWhilemetodo. È essenzialmente filter, ma dovrebbe terminare quando si incontra un elemento che non soddisfa la condizione.

Per esempio:

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)

Funzionerà bene anche per Iterators / Iterables. La soluzione che suggerisco per la tua "somma di numeri pari, ma interrompi su dispari" è:

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

E solo per dimostrare che non sta perdendo tempo una volta che raggiunge un numero dispari ...

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

Questo era esattamente il tipo di semplicità che stavo cercando - grazie!
Tanner

14

Puoi fare quello che vuoi in uno stile funzionale usando la versione pigra di foldRight in scalaz. Per una spiegazione più approfondita, vedere questo post del blog . Sebbene questa soluzione utilizzi un Stream, puoi convertire un Iterablein modo Streamefficiente con 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
})

Questo stampa solo

0
1

che mostra chiaramente che la funzione anonima viene chiamata solo due volte (cioè fino a quando non incontra il numero dispari). Ciò è dovuto alla definizione di foldr, la cui firma (in caso di Stream) è def foldr[B](b: B)(f: (Int, => B) => B)(implicit r: scalaz.Foldable[Stream]): B. Si noti che la funzione anonima accetta un parametro by name come secondo argomento, quindi non è necessario valutarlo.

A proposito, puoi ancora scrivere questo con la soluzione di pattern matching dell'OP, ma trovo if / else e map più eleganti.


Cosa succede se metti printlnprima if- elseespressione?
missingfaktor

@missingfaktor: quindi stampa 0 e 1, ma non di più
Kim Stebel

@missingfaktor: poiché il mio punto è più facile da rendere in questo modo, l'ho cambiato nella risposta
Kim Stebel

1
Nota che puoi trasformare qualsiasi iterabile in uno stream con toStream, quindi questa risposta è più generica di quanto non appaia a prima vista.
Rex Kerr

2
Dato che usi scalaz, perché non usare ‛0.some‛?
pedrofurla

7

Ebbene, Scala consente ritorni non locali. Ci sono opinioni divergenti sul fatto che questo sia un buon stile o meno.

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)

MODIFICARE:

In questo caso particolare, come suggerito da @Arjan, puoi anche fare:

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
invece di Some(0): Option[Int]scrivere puoi semplicemente Option(0).
Luigi Plinge

1
@LuigiPlinge, sì. Ho appena copiato e incollato il codice di OP e ho apportato solo le modifiche necessarie per fare un punto.
Missingfaktor

5

Gatti ha un metodo chiamato foldM che non cortocircuito (per Vector, List, Stream, ...).

Funziona come segue:

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

Non appena uno degli elementi della collezione non è pari, ritorna.


4

Puoi usare foldMda cats lib (come suggerito da @Didac) ma ti suggerisco di usare Eitherinvece di Optionse vuoi ottenere la somma effettiva.

bifoldMapviene utilizzato per estrarre il risultato da 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)
      }
    }
  }

esempi:

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

Sono venuto qui per pubblicare una risposta simile, perché questo è il modo più conveniente ma ancora FP da fare secondo me. Sono sorpreso che nessuno voti per questo. Quindi, prendi il mio +1. (Preferisco (acc + n).asRightinvece Either.right(acc + n)che comunque)
abdolence il

piuttosto che bifoldMapsolo fold(L => C, R => C): Cfunzionerà Either[L, R], e quindi non hai bisogno di unMonoid[C]
Ben Hutchison

1

@ Rex Kerr la tua risposta mi ha aiutato, ma avevo bisogno di modificarla per utilizzare E l'uno o l'altro

  
  def foldOrFail [A, B, C, D] (map: B => Either [D, C]) (merge: (A, C) => A) (initial: A) (it: Iterable [B]): O [D, A] = {
    val ii = it.iterator
    var b = iniziale
    while (ii.hasNext) {
      val x = ii. successivo
      map (x) match {
        case Left (error) => return Left (error)
        case Right (d) => b = merge (b, d)
      }
    }
    Destra (b)
  }

1

Potresti provare a utilizzare una var temporanea e utilizzare takeWhile. Ecco una versione.

  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)

  }

L' evenSumdovrebbe essere Some(20)in questo caso.



0

Una soluzione più bella sarebbe usare span:

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

... ma attraversa l'elenco due volte se tutti i numeri sono pari


2
Mi piace il pensiero laterale esemplificato dalla tua soluzione, ma risolve solo l'esempio specifico scelto nella domanda piuttosto che affrontare la questione generale di come terminare una piega in anticipo.
iainmcgin

volevo mostrare come fare il contrario, non terminare una piega in anticipo ma solo piegare (in questo caso la somma) sui valori che vogliamo piegare
Arjan

0

Solo per ragioni "accademiche" (:

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

Ci vuole due volte, quindi dovrebbe, ma è un bel rivestimento. Se "Chiudi" non viene trovato, tornerà

headers.size

Un altro (migliore) è questo:

var headers = Source.fromFile(file).getLines().next().split(",").toList
var closeHeaderIdx = headers.indexOf("Close")
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.