web-dev-qa-db-fra.com

Scala: supprimer les doublons dans la liste des objets

J'ai une liste d'objets List[Object] qui sont tous instanciés de la même classe. Cette classe a un champ qui doit être unique Object.property. Quelle est la façon la plus propre d'itérer la liste des objets et de supprimer tous les objets (sauf le premier) avec la même propriété?

54
parsa
list.groupBy(_.property).map(_._2.head)

Explication: La méthode groupBy accepte une fonction qui convertit un élément en clé pour le regroupement. _.property Est simplement un raccourci pour elem: Object => elem.property (Le compilateur génère un nom unique, quelque chose comme x$1). Nous avons donc maintenant une carte Map[Property, List[Object]]. Un Map[K,V] Étend Traversable[(K,V)]. Il peut donc être parcouru comme une liste, mais les éléments sont un tuple. Ceci est similaire à la Map#entrySet() de Java. La méthode map crée une nouvelle collection en itérant chaque élément et en lui appliquant une fonction. Dans ce cas, la fonction est _._2.head Qui est l'abréviation de elem: (Property, List[Object]) => elem._2.head. _2 Est juste une méthode de Tuple qui retourne le deuxième élément. Le deuxième élément est List [Object] et head renvoie le premier élément

Pour que le résultat soit un type que vous souhaitez:

import collection.breakOut
val l2: List[Object] = list.groupBy(_.property).map(_._2.head)(breakOut)

Pour expliquer brièvement, map attend en fait deux arguments, une fonction et un objet qui sont utilisés pour construire le résultat. Dans le premier extrait de code, vous ne voyez pas la deuxième valeur car elle est marquée comme implicite et donc fournie par le compilateur à partir d'une liste de valeurs prédéfinies dans la portée. Le résultat est généralement obtenu à partir du conteneur mappé. C'est généralement une bonne chose. map on List renverra List, map on Array renverra Array etc. Dans ce cas cependant, nous voulons exprimer le conteneur que nous voulons comme résultat. C'est là que la méthode breakOut est utilisée. Il construit un générateur (la chose qui génère des résultats) en ne regardant que le type de résultat souhaité. C'est une méthode générique et le compilateur déduit ses types génériques parce que nous avons explicitement tapé l2 pour être List[Object] Ou pour conserver l'ordre (en supposant que Object#property Est de type Property):

list.foldRight((List[Object](), Set[Property]())) {
  case (o, cum@(objects, props)) => 
    if (props(o.property)) cum else (o :: objects, props + o.property))
}._1

foldRight est une méthode qui accepte un résultat initial et une fonction qui accepte un élément et renvoie un résultat mis à jour. La méthode itère chaque élément, mettant à jour le résultat en appliquant la fonction à chaque élément et en renvoyant le résultat final. Nous allons de droite à gauche (plutôt que de gauche à droite avec foldLeft) parce que nous ajoutons à objects - c'est O (1), mais l'ajout est O (N). Observez également le bon style ici, nous utilisons une correspondance de motifs pour extraire les éléments.

Dans ce cas, le résultat initial est une paire (Tuple) d'une liste vide et un ensemble. La liste est le résultat qui nous intéresse et l'ensemble est utilisé pour garder une trace des propriétés que nous avons déjà rencontrées. Dans chaque itération, nous vérifions si l'ensemble props contient déjà la propriété (dans Scala, obj(x) est traduit en obj.apply(x). Dans Set, la méthode apply est def apply(a: A): Boolean. Autrement dit, accepte un élément et renvoie vrai/faux s'il existe ou non). Si la propriété existe (déjà rencontrée), le résultat est renvoyé tel quel. Sinon, le résultat est mis à jour pour contenir l'objet (o :: objects) Et la propriété est enregistrée (props + o.property)

Mise à jour: @andreypopp voulait une méthode générique:

import scala.collection.IterableLike
import scala.collection.generic.CanBuildFrom

class RichCollection[A, Repr](xs: IterableLike[A, Repr]){
  def distinctBy[B, That](f: A => B)(implicit cbf: CanBuildFrom[Repr, A, That]) = {
    val builder = cbf(xs.repr)
    val i = xs.iterator
    var set = Set[B]()
    while (i.hasNext) {
      val o = i.next
      val b = f(o)
      if (!set(b)) {
        set += b
        builder += o
      }
    }
    builder.result
  }
}

implicit def toRich[A, Repr](xs: IterableLike[A, Repr]) = new RichCollection(xs)

utiliser:

scala> list.distinctBy(_.property)
res7: List[Obj] = List(Obj(1), Obj(2), Obj(3))

Notez également que cela est assez efficace car nous utilisons un générateur. Si vous avez de très grandes listes, vous souhaiterez peut-être utiliser un HashSet mutable au lieu d'un ensemble régulier et comparer les performances.

123
IttayD

Voici une solution un peu sournoise mais rapide qui préserve l'ordre:

list.filterNot{ var set = Set[Property]()
    obj => val b = set(obj.property); set += obj.property; b}

Bien qu'il utilise en interne un var, je pense qu'il est plus facile à comprendre et à lire que la solution foldLeft.

14
Landei

Démarrage Scala 2.13, la plupart des collections sont désormais fournies avec une méthode distinctBy qui renvoie tous les éléments de la séquence en ignorant les doublons après avoir appliqué une fonction de transformation donnée:

list.distinctBy(_.property)

Par exemple:

List(("a", 2), ("b", 2), ("a", 5)).distinctBy(_._1) // List((a,2), (b,2))
List(("a", 2.7), ("b", 2.1), ("a", 5.4)).distinctBy(_._2.floor) // List((a,2.7), (a,5.4))
12
Xavier Guihot

Encore une solution

@tailrec
def collectUnique(l: List[Object], s: Set[Property], u: List[Object]): List[Object] = l match {
  case Nil => u.reverse
  case (h :: t) => 
    if (s(h.property)) collectUnique(t, s, u) else collectUnique(t, s + h.prop, h :: u)
}
6
walla

Avec ordre de conservation:

def distinctBy[L, E](list: List[L])(f: L => E): List[L] =
  list.foldLeft((Vector.empty[L], Set.empty[E])) {
    case ((acc, set), item) =>
      val key = f(item)
      if (set.contains(key)) (acc, set)
      else (acc :+ item, set + key)
  }._1.toList

distinctBy(list)(_.property)
5
Timothy Klim

J'ai trouvé un moyen de le faire fonctionner avec groupBy, avec une étape intermédiaire:

def distinctBy[T, P, From[X] <: TraversableLike[X, From[X]]](collection: From[T])(property: T => P): From[T] = {
  val uniqueValues: Set[T] = collection.groupBy(property).map(_._2.head)(breakOut)
  collection.filter(uniqueValues)
}

Utilisez-le comme ceci:

scala> distinctBy(List(redVolvo, bluePrius, redLeon))(_.color)
res0: List[Car] = List(redVolvo, bluePrius)

Similaire à la première solution d'IttayD, mais il filtre la collection d'origine en fonction de l'ensemble de valeurs uniques. Si mes attentes sont correctes, cela fait trois traversées: une pour groupBy, une pour map et une pour filter. Il conserve l'ordre de la collection d'origine, mais ne prend pas nécessairement la première valeur pour chaque propriété. Par exemple, il aurait pu renvoyer List(bluePrius, redLeon) à la place.

Bien sûr, la solution d'IttayD est encore plus rapide car elle ne fait qu'une seule traversée.

Ma solution présente également l'inconvénient que, si la collection a Cars qui sont en fait les mêmes, les deux seront dans la liste de sortie. Cela pourrait être corrigé en supprimant filter et en retournant uniqueValues directement, avec le type From[T]. Cependant, il semble que CanBuildFrom[Map[P, From[T]], T, From[T]] N'existe pas ... les suggestions sont les bienvenues!

2
Jodiug

Beaucoup de bonnes réponses ci-dessus. Cependant, distinctBy est déjà dans Scala, mais dans un endroit pas si évident. Vous pouvez peut-être l'utiliser comme

def distinctBy[A, B](xs: List[A])(f: A => B): List[A] =
  scala.reflect.internal.util.Collections.distinctBy(xs)(f)
2
Abel Terefe