Scala Partition/Collect Usage

Czy można użyć jednego wywołania do collect, aby utworzyć 2 nowe listy? Jeśli nie, jak Mogę to zrobić używając partition?

Author: Dan Getz, 2011-01-24

5 answers

collect (zdefiniowane na TraversableLike i dostępne we wszystkich podklasach) działa z kolekcją i PartialFunction. Tak się też składa, że kilka klauzul przypadków zdefiniowanych wewnątrz klamer jest funkcją częściową (patrz sekcja 8.5 specyfikacji języka Scala [Ostrzeżenie-PDF])

Jak w obsłudze WYJĄTKÓW:

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

Jest to przydatny sposób definiowania funkcji, która będzie akceptować tylko niektóre wartości danego typu.

Rozważ użycie go na lista wartości mieszanych:

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
}

Argumentem metody to to collect jest PartialFunction[Any,String]. PartialFunction ponieważ nie jest zdefiniowany dla wszystkich możliwych wejść typu Any (który jest typem List) i String ponieważ to jest to, co zwracają wszystkie klauzule.

Jeśli spróbujesz użyć map zamiast collect, Podwójna wartość na końcu mixedList spowodowałaby MatchError. Użycie collect po prostu odrzuca tę, jak również każdą inną wartość, dla której funkcja częściowa nie jest zdefiniowana.

Jeden możliwe byłoby zastosowanie innej logiki do elementów listy:

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

Chociaż jest to tylko przykład, używanie zmiennych zmiennych, takich jak ta, jest uważane przez wielu za zbrodnię wojenną - więc proszę, nie róbcie tego!

A dużo lepszym rozwiązaniem jest użycie collect dwa razy:

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

Lub jeśli wiesz na pewno, że lista zawiera tylko dwa typy wartości, możesz użyć partition, która dzieli zbiory na wartości w zależności od tego, czy pasują do niektórych predykat:

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

Haczyk polega na tym, że zarówno strings, jak i ints są typu List[Any], choć można je łatwo zmusić do czegoś więcej typów (być może za pomocą collect...)

Jeśli posiadasz już kolekcję type-safe I chcesz podzielić się na inną własność elementów, wtedy jest ci trochę łatwiej:

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

Mam nadzieję, że to podsumuje, jak te dwie metody mogą Ci pomóc tutaj!

 76
Author: Kevin Wright,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-24 16:56:17

Nie wiem, jak to zrobić z collect bez użycia mutowalnych list, ale partition może również używać dopasowania wzorców (tylko trochę bardziej wyrazistych)

List("a", 1, 2, "b", 19).partition { 
  case s:String => true
  case _ => false 
}
 6
Author: Adam Rabung,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-12-29 18:43:22

Podpis normalnie używanego collect na, powiedzmy, Seq, to

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

Co jest naprawdę szczególnym przypadkiem

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

Więc jeśli używasz go w trybie domyślnym, odpowiedź brzmi nie, z pewnością nie: otrzymujesz z niego dokładnie jedną sekwencję. Jeśli zastosujesz CanBuildFrom przez Builder, zobaczysz, że możliwe byłoby, aby That były w rzeczywistości dwiema sekwencjami, ale nie można powiedzieć, do której sekwencji powinien wejść element, ponieważ funkcja częściowa może tylko powiedzieć " tak, należę" albo "nie, nie należę".

Więc co zrobić, jeśli chcesz mieć wiele warunków, które skutkują podzieleniem listy na kilka różnych części? Jednym ze sposobów jest utworzenie funkcji wskaźnika A => Int, gdzie twoja A jest mapowana do numerowanej klasy, a następnie użyj groupBy. Na przykład:

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

Teraz możesz przeglądać swoje listy podrzędne według klas (w tym przypadku 0, 1 i 2). Niestety, jeśli chcesz zignorować niektóre wejścia, nadal musisz umieścić je w klasie (np. prawdopodobnie nie obchodzi mnie wiele kopii None w tym przypadku).

 5
Author: Rex Kerr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-24 17:03:20

Używam tego. Jedną miłą rzeczą jest to, że łączy partycjonowanie i mapowanie w jednej iteracji. Jedną z wad jest to, że przydziela kilka tymczasowych obiektów (instancje Either.Left i Either.Right)

/**
 * 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)
 4
Author: Alex Cruise,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-25 18:15:31

Nie mogłem znaleźć satysfakcjonującego rozwiązania tego podstawowego problemu. Nie potrzebuję wykładu o collect i nie obchodzi mnie, czy to czyjeś zadanie domowe. Poza tym, nie chcę czegoś, co działa tylko dla List.

Oto moje zadanie. Wydajny i kompatybilny z dowolnymi TraversableOnce, nawet ciągami:
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
  }
}

Przykładowe zastosowania:

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])
 1
Author: LP_,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-05-26 19:52:50