web-dev-qa-db-fra.com

Abandonner tôt dans un pli

Quelle est la meilleure façon de terminer un pli plus tôt? À titre d'exemple simplifié, imaginez que je veux résumer les nombres dans un Iterable, mais si je rencontre quelque chose que je n'attends pas (par exemple un nombre impair), je pourrais vouloir terminer. Ceci est une première approximation

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

Cependant, cette solution est assez moche (comme dans, si je faisais un .foreach et un retour - ce serait beaucoup plus propre et plus clair) et pire que tout, elle parcourt tout l'itérable même si elle rencontre un nombre non pair .

Alors, quelle serait la meilleure façon d'écrire un pli comme celui-ci, qui se termine tôt? Dois-je simplement écrire ceci de manière récursive, ou existe-t-il un moyen plus accepté?

79
Heptic

Mon premier choix serait généralement d'utiliser la récursivité. Il n'est que modérément moins compact, est potentiellement plus rapide (certainement pas plus lent) et, en cas de résiliation anticipée, peut rendre la logique plus claire. Dans ce cas, vous avez besoin de définitions imbriquées, ce qui est un peu gênant:

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

Mon deuxième choix serait d'utiliser return, car il garde tout le reste intact et vous n'avez qu'à envelopper le pli dans un def pour avoir quelque chose à retourner - dans ce cas, vous ont déjà une méthode, donc:

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

qui dans ce cas particulier est beaucoup plus compact que la récursion (bien que nous ayons été particulièrement malchanceux avec la récursivité car nous avons dû faire une transformation itérable/itérateur). Le flux de contrôle nerveux est quelque chose à éviter quand tout le reste est égal, mais ici ce n'est pas le cas. Pas de mal à l'utiliser dans les cas où il est précieux.

Si je faisais cela souvent et que je le voulais au milieu d'une méthode quelque part (donc je ne pouvais pas simplement utiliser return), j'utiliserais probablement la gestion des exceptions pour générer un flux de contrôle non local. C'est, après tout, ce qu'il est bon, et la gestion des erreurs n'est pas le seul moment où il est utile. La seule astuce consiste à éviter de générer une trace de pile (ce qui est vraiment lent), et c'est facile car le trait NoStackTrace et son trait enfant ControlThrowable le font déjà pour vous. Scala l'utilise déjà en interne (en fait, c'est comme ça qu'il implémente le retour de l'intérieur du pli!). Faisons le nôtre (ne peut pas être imbriqué, bien que l'on puisse corriger cela):

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

Ici, bien sûr, utiliser return est mieux, mais notez que vous pouvez mettre shortcut n'importe où, et pas seulement encapsuler une méthode entière.

La prochaine étape pour moi serait de réimplémenter fold (soit moi-même, soit de trouver une bibliothèque qui le fait) afin qu'il puisse signaler une résiliation anticipée. Les deux façons naturelles de procéder sont de ne pas propager la valeur mais un Option contenant la valeur, où None signifie la terminaison; ou pour utiliser une deuxième fonction d'indicateur qui signale l'achèvement. Le pli paresseux Scalaz montré par Kim Stebel couvre déjà le premier cas, donc je vais montrer le second (avec une implémentation mutable):

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

(Que vous mettiez en œuvre la résiliation par récursivité, retour, paresse, etc., cela dépend de vous.)

Je pense que cela couvre les principales variantes raisonnables; il existe également d'autres options, mais je ne sais pas pourquoi on les utiliserait dans ce cas. (Iterator lui-même fonctionnerait bien s'il avait un findOrPrevious, mais ce n'est pas le cas, et le travail supplémentaire qu'il faut pour le faire à la main en fait une option idiote à utiliser ici.)

60
Rex Kerr

Le scénario que vous décrivez (quitter une condition indésirable) semble être un bon cas d'utilisation pour la méthode takeWhile. Il s'agit essentiellement de filter, mais devrait se terminer en rencontrant un élément qui ne remplit pas la condition.

Par exemple:

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)

Cela fonctionnera très bien pour Iterators/Iterables aussi. La solution que je propose pour votre "somme des nombres pairs, mais cassez les nombres impairs" est:

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

Et juste pour prouver que ça ne fait pas perdre votre temps une fois qu'il atteint un nombre impair ...

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
23
Dylan

Vous pouvez faire ce que vous voulez dans un style fonctionnel en utilisant la version paresseuse de foldRight dans scalaz. Pour une explication plus approfondie, voir cet article de blog . Bien que cette solution utilise un Stream, vous pouvez convertir un Iterable en Stream efficacement avec 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
})

Cela imprime uniquement

0
1

ce qui montre clairement que la fonction anonyme n'est appelée que deux fois (c'est-à-dire jusqu'à ce qu'elle rencontre le nombre impair). Cela est dû à la définition de foldr, dont la signature (dans le cas de Stream) est def foldr[B](b: B)(f: (Int, => B) => B)(implicit r: scalaz.Foldable[Stream]): B. Notez que la fonction anonyme prend un paramètre par nom comme deuxième argument, il n'a donc pas besoin d'être évalué.

Btw, vous pouvez toujours écrire ceci avec la solution de correspondance de motifs de l'OP, mais je trouve si/else et une carte plus élégante.

14
Kim Stebel

Eh bien, Scala autorise les retours non locaux. Il existe des opinions divergentes quant à savoir si c'est un bon style ou non.

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)

MODIFIER:

Dans ce cas particulier, comme l'a suggéré @Arjan, vous pouvez également:

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

Cats a une méthode appelée foldM qui fait les courts-circuits (pour Vector, List, Stream, .. .).

Cela fonctionne comme suit:

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

Dès qu'un des éléments de la collection n'est pas pair, il revient.

4
Didac Montero

Vous pouvez essayer d'utiliser une variable temporaire et d'utiliser takeWhile. Voici une version.

  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 doit être Some(20) dans ce cas.

1
seagull1089

@Rex Kerr, votre réponse m'a aidé, mais je devais l'ajuster pour utiliser soit

  
 def foldOrFail [A, B, C, D] (carte: B => Soit [D, C]) (fusion: (A, C) => A) (initial: A) (it: Iterable [B]): Soit [D, A] = {
 Val ii = it.iterator 
 Var b = initial 
 While (ii.hasNext) {
 val x = ii.next 
 map (x) correspond à {
 cas Left (error) => return Left (error) 
 case Right (d) => b = merge ( b, d) 
} 
} 
 Droite (b) 
} 
1
Core

Une meilleure solution serait d'utiliser span:

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

... mais il parcourt la liste deux fois si tous les nombres sont pairs

0
Arjan

Juste pour des raisons "académiques" (:

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

Prend deux fois alors il devrait mais c'est une belle doublure. Si "Fermer" n'est pas trouvé, il reviendra

headers.size

Un autre (meilleur) est celui-ci:

var headers = Source.fromFile(file).getLines().next().split(",").toList
var closeHeaderIdx = headers.indexOf("Close")
0
ozma

Vous pouvez lever une exception bien choisie lorsque vous rencontrez votre critère de terminaison, en le traitant dans le code appelant.

0
waldrumpus