web-dev-qa-db-ja.com

Scala:オブジェクトのリストから重複を削除します

オブジェクトのリストを持っていますList[Object]これらはすべて同じクラスからインスタンス化されます。このクラスには一意のフィールドが必要ですObject.property。オブジェクトのリストを反復処理し、同じプロパティを持つすべてのオブジェクト(ただし最初のオブジェクト)を削除する最もクリーンな方法は何ですか?

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

説明:groupByメソッドは、グループ化のために要素をキーに変換する関数を受け入れます。 __.property_は、単に_elem: Object => elem.property_の省略形です(コンパイラーは_x$1_のような一意の名前を生成します)。これで、マップ_Map[Property, List[Object]]_ができました。 _Map[K,V]_はTraversable[(K,V)]を拡張します。そのため、リストのように走査できますが、要素はタプルです。これは、JavaのMap#entrySet()に似ています。 mapメソッドは、各要素を繰り返して関数を適用することにより、新しいコレクションを作成します。この場合、関数はelem: (Property, List[Object]) => elem._2.headの省略形である__._2.head_です。 __2_は、2番目の要素を返すTupleの単なるメソッドです。 2番目の要素はList [Object]で、headは最初の要素を返します

結果を希望する型にするには:

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

簡単に説明すると、mapは実際には2つの引数、関数と結果を構築するために使用されるオブジェクトを想定しています。最初のコードスニペットでは、2番目の値は暗黙的としてマークされているため、スコープ内の事前定義値のリストからコンパイラーによって提供されるため、2番目の値は表示されません。通常、結果はマップされたコンテナから取得されます。これは通常良いことです。リスト上のマップはリストを返し、配列上のマップは配列などを返します。しかし、この場合、結果として必要なコンテナを表現したいと思います。ここでbreakOutメソッドが使用されます。目的の結果タイプのみを表示することにより、ビルダー(結果を構築するもの)を構築します。これはジェネリックメソッドであり、コンパイラーは、l2を明示的に_List[Object]_と入力するか、順序を保持するためにジェネリック型を推測します(_Object#property_は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は、初期結果を受け入れるメソッドと、要素を受け入れて更新された結果を返す関数です。このメソッドは各要素を繰り返し、関数を各要素に適用して結果を更新し、最終結果を返します。 foldLeftの前に追加するため、右から左に(objectsで左から右にではなく)移動します。これはO(1)ですが、追加はO(N)です。また、ここで適切なスタイリングを確認してください。パターンマッチを使用して要素を抽出しています。

この場合、初期結果は空のリストとセットのペア(タプル)です。リストは、私たちが興味を持っている結果であり、このセットは、すでに遭遇したプロパティを追跡するために使用されます。各反復で、セットpropsにすでにプロパティが含まれているかどうかを確認します(Scalaでは、obj(x)obj.apply(x)に変換されます。Setでは、メソッドapplydef apply(a: A): Booleanです。つまり、要素を受け入れ、存在するかどうかに応じてtrue/falseを返します。プロパティが存在する場合(既に検出されている場合)、結果はそのまま返されます。それ以外の場合、結果が更新されてオブジェクト(_o :: objects_)が含まれ、プロパティが記録されます(_props + o.property_)

更新:@andreypoppは一般的なメソッドが必要でした:

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

使用する:

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

また、ビルダーを使用しているため、これは非常に効率的です。リストが非常に大きい場合は、通常のセットの代わりに可変HashSetを使用して、パフォーマンスをベンチマークすることができます。

123
IttayD

順序を保持する少し卑劣だが高速なソリューションを次に示します。

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

内部でvarを使用していますが、foldLeft-solutionよりも理解しやすく、読みやすいと思います。

14
Landei

開始Scala 2.13、ほとんどのコレクションに distinctBy メソッドが追加されました。このメソッドは、特定の変換関数を適用した後、重複を無視してシーケンスのすべての要素を返します。

list.distinctBy(_.property)

例えば:

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

もう一つのソリューション

@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

順序を保持する場合:

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

私は、1つの中間ステップで、groupByで動作させる方法を見つけました:

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

次のように使用します。

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

IttayDの最初のソリューションに似ていますが、一意の値のセットに基づいて元のコレクションをフィルタリングします。私の期待が正しい場合、これは3つのトラバーサルを実行します。1つはgroupBy、1つはmap、1つはfilterです。元のコレクションの順序を維持しますが、必ずしも各プロパティの最初の値を取得するとは限りません。たとえば、代わりにList(bluePrius, redLeon)を返した可能性があります。

もちろん、IttayDのソリューションは、たった1つのトラバーサルを行うため、さらに高速です。

私のソリューションには、コレクションに実際に同じCarsがある場合、両方が出力リストに含まれるという欠点もあります。これは、filterを削除し、タイプ_From[T]_でuniqueValuesを直接返すことで修正できます。ただし、_CanBuildFrom[Map[P, From[T]], T, From[T]]_は存在しないようです...提案を歓迎します!

2
Jodiug

上記の多くの良い答え。ただし、distinctByは既にScalaにありますが、それほど明白ではありません。おそらくあなたはそれを次のように使うことができます

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