web-dev-qa-db-fra.com

Scala: Peut-il y avoir une quelconque raison de préférer `filter + map` à` collect`?

Peut-il y avoir une raison de préférer filter+map

list.filter (i => aCondition(i)).map(i => fun(i))

sur collect? : 

list.collect(case i if aCondition(i) => fun(i))

Celui avec collect (look unique) me semble plus rapide et plus propre. Donc, je choisirais toujours collect

23
Daniel

La plupart des collections de Scala appliquent avec empressement des opérations et (sauf si vous utilisez une bibliothèque de macros qui le fait pour vous) ne fusionnera pas les opérations. Ainsi, filter suivi de map créera généralement deux collections (et même si vous utilisez Iterator ou une telle méthode, la forme intermédiaire sera créée de manière transitoire, bien qu’un élément à la fois), alors que collect ne le sera pas.

Par contre, collect utilise une fonction partielle pour implémenter le test conjoint, et les fonctions partielles sont plus lentes que les prédicats (A => Boolean) pour vérifier si quelque chose se trouve dans la collection.

En outre, dans certains cas, il est tout simplement plus simple de lire l'un que l'autre sans se soucier des performances ou des différences d'utilisation de la mémoire d'un facteur 2 ou plus. Dans ce cas, utilisez ce qui est le plus clair. Généralement, si vous avez déjà nommé les fonctions, il est plus clair de lire

xs.filter(p).map(f)
xs.collect{ case x if p(x) => f(x) }

mais si vous fournissez les fermetures en ligne, collect paraît généralement plus propre

xs.filter(x < foo(x, x)).map(x => bar(x, x))
xs.collect{ case x if foo(x, x) => bar(x, x) }

même si ce n'est pas nécessairement plus court, car vous ne faites référence à la variable qu'une seule fois.

Maintenant, quelle est la différence de performance? Cela varie, mais si nous considérons une collection comme celle-ci:

val v = Vector.tabulate(10000)(i => ((i%100).toString, (i%7).toString))

et vous voulez choisir la deuxième entrée en fonction du filtrage de la première (pour que les opérations de filtrage et de cartographie soient vraiment faciles), nous obtenons le tableau suivant.

Remarque: vous pouvez obtenir des vues paresseuses dans des collections et y regrouper des opérations. Vous ne récupérez pas toujours votre type d'origine, mais vous pouvez toujours utiliser to pour obtenir le bon type de collection. Donc, xs.view.filter(p).map(f).toVector, à cause de la vue, ne créerait pas d'intermédiaire. Cela est testé ci-dessous aussi. Il a également été suggéré que l'on peut xs.flatMap(x => if (p(x)) Some(f(x)) else None) et que ceci est efficient. Ce n'est pas le cas. Il est également testé ci-dessous. Et vous pouvez éviter la fonction partielle en créant explicitement un générateur: val vb = Vector.newBuilder[String]; xs.foreach(x => if (p(x)) vb += f(x)); vb.result, et les résultats correspondants sont également répertoriés ci-dessous.

Dans le tableau ci-dessous, trois conditions ont été testées: ne rien filtrer, ne filtrer que la moitié, tout filtrer. Les temps ont été normalisés pour filtrer/mapper (100% = même temps que le filtre/mapper, plus bas est meilleur). Les limites d'erreur sont autour de + - 3%.

Performances de différentes alternatives de filtre/carte

====================== Vector ========================
filter/map   collect  view filt/map  flatMap   builder
   100%        44%          64%        440%      30%    filter out none
   100%        60%          76%        605%      42%    filter out half
   100%       112%         103%       1300%      74%    filter out all

Ainsi, filter/map et collect sont généralement assez proches (avec collect gagnant lorsque vous en gardez beaucoup), flatMap est beaucoup plus lent dans toutes les situations et la création d'un générateur gagne toujours. (Ceci est vrai spécifiquement pour Vector. D'autres collections peuvent avoir des caractéristiques quelque peu différentes, mais les tendances pour la plupart seront similaires car les différences dans les opérations sont similaires.) Les vues dans le test ont tendance à être gagnantes. , mais ils ne fonctionnent pas toujours de manière transparente (et ils ne sont pas vraiment meilleurs que collect sauf pour le cas vide).

Donc, en bout de ligne: préférez filter puis map si cela facilite la clarté lorsque la vitesse importe peu, ou préférez-le pour la vitesse lorsque vous filtrez presque tout, mais que vous souhaitez tout de même garder les éléments fonctionnels (évitez donc d'utiliser un générateur ) et sinon, utilisez collect.

38
Rex Kerr

Je suppose que ceci est plutôt basé sur l'opinion, mais étant donné les définitions suivantes:

scala> val l = List(1,2,3,4)
l: List[Int] = List(1, 2, 3, 4)

scala> def f(x: Int) = 2*x
f: (x: Int)Int

scala> def p(x: Int) = x%2 == 0
p: (x: Int)Boolean

Lequel des deux trouvez-vous plus agréable à lire:

l.filter(p).map(f)

ou

l.collect{ case i if p(i) => f(i) }

(Notez que je devais corriger votre syntaxe ci-dessus, car vous avez besoin du crochet et de case pour ajouter la condition if).

Personnellement, je trouve la filter + map beaucoup plus agréable à lire et à comprendre. Tout dépend de la syntaxe exacte que vous utilisez, mais étant donné p et f, vous n'avez pas à écrire de fonctions anonymes lorsque vous utilisez filter ou map, vous en avez besoin lorsque vous utilisez collect.

Vous avez également la possibilité d'utiliser flatMap:

l.flatMap(i => if(p(i)) Some(f(i)) else None)

Laquelle est probablement la plus efficace parmi les 3 solutions, mais je la trouve moins agréable que map et filter.

Globalement, il est très difficile de dire laquelle serait la plus rapide, car cela dépend en grande partie des optimisations qui seront finalement exécutées par scalac, puis par la JVM. Tous les 3 devraient être assez proches, et certainement pas un facteur pour décider lequel utiliser.

3
Regis Blanc

Un cas où filter/map est plus propre, c'est quand vous voulez aplatir le résultat du filtre.

def getList(x: Int) = {
  List.range(x, 0, -1)
}

val xs = List(1,2,3,4)

//Using filter and flatMap
xs.filter(_ % 2 == 0).flatMap(getList)

//Using collect and flatten
xs.collect{ case x if x % 2 == 0 => getList(x)}.flatten
0
Apoorv Shrivastava