web-dev-qa-db-fra.com

Scala Partition / Collect Usage

Est-il possible d'utiliser un seul appel à collect pour créer 2 nouvelles listes? Sinon, comment puis-je faire cela en utilisant partition?

37
Adrian Modliszewski

collect (défini sur TraversableLike et disponible dans toutes les sous-classes) fonctionne avec une collection et un PartialFunction. Il se trouve également qu'un groupe de clauses de casse définies entre accolades est une fonction partielle (voir la section 8.5 des Spécification du langage Scala [avertissement - PDF] )

Comme dans la gestion des exceptions:

try {
  ... do something risky ...
} catch {
  //The contents of this catch block are a partial function
  case e: IOException => ...
  case e: OtherException => ...
}

C'est un moyen pratique de définir une fonction qui n'acceptera que certaines valeurs d'un type donné.

Pensez à l'utiliser sur une liste de valeurs mixtes:

val mixedList = List("a", 1, 2, "b", 19, 42.0) //this is a List[Any]
val results = mixedList collect {
  case s: String => "String:" + s
  case i: Int => "Int:" + i.toString
}

L'argument de la méthode to collect est un PartialFunction[Any,String]. PartialFunction car il n'est pas défini pour toutes les entrées possibles de type Any (c'est le type du List) et String car c'est ce que toutes les clauses renvoient .

Si vous essayez d'utiliser map au lieu de collect, la valeur double à la fin de mixedList provoquera un MatchError. L'utilisation de collect supprime simplement cela, ainsi que toute autre valeur pour laquelle la fonction partielle n'est pas définie.

Une utilisation possible serait d'appliquer une logique différente aux éléments de la liste:

var strings = List.empty[String]
var ints = List.empty[Int]
mixedList collect {
  case s: String => strings :+= s
  case i: Int => ints :+= i
}

Bien que ce ne soit qu'un exemple, l'utilisation de variables mutables comme celle-ci est considérée par beaucoup comme un crime de guerre - Alors, ne le faites pas!

Une meilleure meilleure solution consiste à utiliser collecter deux fois:

val strings = mixedList collect { case s: String => s }
val ints = mixedList collect { case i: Int => i }

Ou si vous savez avec certitude que la liste ne contient que deux types de valeurs, vous pouvez utiliser partition, qui divise les collections en valeurs selon qu'elles correspondent ou non à un prédicat:

//if the list only contains Strings and Ints:
val (strings, ints) = mixedList partition { case s: String => true; case _ => false }

Le hic ici est que strings et ints sont de type List[Any], bien que vous puissiez facilement les contraindre à quelque chose de plus sûr (peut-être en utilisant collect...)

Si vous avez déjà une collection de type sécurisé et que vous souhaitez partager une autre propriété des éléments, les choses sont un peu plus faciles pour vous:

val intList = List(2,7,9,1,6,5,8,2,4,6,2,9,8)
val (big,small) = intList partition (_ > 5)
//big and small are both now List[Int]s

J'espère que cela résume comment les deux méthodes peuvent vous aider ici!

78
Kevin Wright

Je ne sais pas comment le faire avec collect sans utiliser de listes mutables, mais partition peut également utiliser la correspondance de modèle (juste un peu plus verbeux)

List("a", 1, 2, "b", 19).partition { 
  case s:String => true
  case _ => false 
}
7
Adam Rabung

La signature du collect normalement utilisé sur, disons, Seq, est

collect[B](pf: PartialFunction[A,B]): Seq[B]

ce qui est vraiment un cas particulier de

collect[B, That](pf: PartialFunction[A,B])(
  implicit bf: CanBuildFrom[Seq[A], B, That]
): That

Donc, si vous l'utilisez en mode par défaut, la réponse est non, certainement pas: vous en tirez exactement une séquence. Si vous suivez CanBuildFrom à Builder, vous voyez qu'il serait possible de faire de That en fait deux séquences, mais il n'y aurait aucun moyen de savoir quelle séquence un élément devrait entrer, car la fonction partielle ne peut dire que "oui, j'appartiens" ou "non, je n'appartiens pas".

Alors, que faites-vous si vous souhaitez que plusieurs conditions entraînent la division de votre liste en plusieurs morceaux différents? Une façon consiste à créer une fonction d'indicateur A => Int, où votre A est mappé dans une classe numérotée, puis utilisez groupBy. Par exemple:

def optionClass(a: Any) = a match {
  case None => 0
  case Some(x) => 1
  case _ => 2
}
scala> List(None,3,Some(2),5,None).groupBy(optionClass)
res11: scala.collection.immutable.Map[Int,List[Any]] = 
  Map((2,List(3, 5)), (1,List(Some(2))), (0,List(None, None)))

Vous pouvez maintenant rechercher vos sous-listes par classe (0, 1 et 2 dans ce cas). Malheureusement, si vous voulez ignorer certaines entrées, vous devez toujours les mettre dans une classe (par exemple, vous ne vous souciez probablement pas des multiples copies de None dans ce cas).

6
Rex Kerr

J'utilise ça. Une bonne chose à ce sujet est qu'il combine le partitionnement et le mappage en une seule itération. Un inconvénient est qu'il alloue un tas d'objets temporaires (le Either.Left et Either.Right instances)

/**
 * Splits the input list into a list of B's and a list of C's, depending on which type of value the mapper function returns.
 */
def mapSplit[A,B,C](in: List[A])(mapper: (A) => Either[B,C]): (List[B], List[C]) = {
  @tailrec
  def mapSplit0(in: List[A], bs: List[B], cs: List[C]): (List[B], List[C]) = {
    in match {
      case a :: as =>
        mapper(a) match {
          case Left(b)  => mapSplit0(as, b :: bs, cs     )
          case Right(c) => mapSplit0(as, bs,      c :: cs)
        }
      case Nil =>
        (bs.reverse, cs.reverse)
    }
  }

  mapSplit0(in, Nil, Nil)
}

val got = mapSplit(List(1,2,3,4,5)) {
  case x if x % 2 == 0 => Left(x)
  case y               => Right(y.toString * y)
}

assertEquals((List(2,4),List("1","333","55555")), got)
5
Alex Cruise

Démarrage Scala 2.13, la plupart des collections sont désormais fournies avec une méthode partitionMap qui partitionne les éléments en fonction d'une fonction qui renvoie Right ou Left.

Cela nous permet de faire correspondre les modèles en fonction du type (qui, en tant que collect, permet d'avoir des types spécifiques dans les listes partitionnées) ou de tout autre modèle:

 val (strings, ints) =
   List("a", 1, 2, "b", 19).partitionMap {
     case s: String => Left(s)
     case x: Int    => Right(x)
   }
 // strings: List[String] = List("a", "b")
 // ints: List[Int] = List(1, 2, 19)
2
Xavier Guihot

Je n'ai pas pu trouver de solution satisfaisante à ce problème fondamental ici. Je n'ai pas besoin d'une conférence sur collect et je me fiche que ce soit les devoirs de quelqu'un. De plus, je ne veux pas quelque chose qui ne fonctionne que pour List.

Voici donc mon coup de couteau. Efficace et compatible avec toutes les chaînes de TraversableOnce, même:

implicit class TraversableOnceHelper[A,Repr](private val repr: Repr)(implicit isTrav: Repr => TraversableOnce[A]) {

  def collectPartition[B,Left](pf: PartialFunction[A, B])
  (implicit bfLeft: CanBuildFrom[Repr, B, Left], bfRight: CanBuildFrom[Repr, A, Repr]): (Left, Repr) = {
    val left = bfLeft(repr)
    val right = bfRight(repr)
    val it = repr.toIterator
    while (it.hasNext) {
      val next = it.next
      if (!pf.runWith(left += _)(next)) right += next
    }
    left.result -> right.result
  }

  def mapSplit[B,C,Left,Right](f: A => Either[B,C])
  (implicit bfLeft: CanBuildFrom[Repr, B, Left], bfRight: CanBuildFrom[Repr, C, Right]): (Left, Right) = {
    val left = bfLeft(repr)
    val right = bfRight(repr)
    val it = repr.toIterator
    while (it.hasNext) {
      f(it.next) match {
        case Left(next) => left += next
        case Right(next) => right += next
      }
    }
    left.result -> right.result
  }
}

Exemples d'utilisations:

val (syms, ints) =
  Seq(Left('ok), Right(42), Right(666), Left('ko), Right(-1)) mapSplit identity

val ctx = Map('a -> 1, 'b -> 2) map {case(n,v) => n->(n,v)}
val (bound, unbound) = Vector('a, 'a, 'c, 'b) collectPartition ctx
println(bound: Vector[(Symbol, Int)], unbound: Vector[Symbol])
2
LP_

Quelque chose comme ça pourrait aider

def partitionMap[IN, A, B](seq: Seq[IN])(function: IN => Either[A, B]): (Seq[A], Seq[B]) = {
  val (eitherLeft, eitherRight) = seq.map(function).partition(_.isLeft)
  eitherLeft.map(_.left.get) -> eitherRight.map(_.right.get)
}

Pour l'appeler

val seq: Seq[Any] = Seq(1, "A", 2, "B")
val (ints, strings) = CollectionUtils.partitionMap(seq) {
  case int: Int    => Left(int)
  case str: String => Right(str)
}
ints shouldBe Seq(1, 2)
strings shouldBe Seq("A", "B")

Advantage est une API simple, similaire à celle de Scala 2.12

Désavantage; la collection est exécutée deux fois et il manque la prise en charge de CanBuildFrom

0
m3th0dman