list - Scala Partition/Collect Usage
(5)
La firma de la collect
normalmente utilizada en, por ejemplo, Seq
, es
collect[B](pf: PartialFunction[A,B]): Seq[B]
que es realmente un caso particular de
collect[B, That](pf: PartialFunction[A,B])(
implicit bf: CanBuildFrom[Seq[A], B, That]
): That
Entonces, si lo usa en modo predeterminado, la respuesta es no, seguramente no: obtiene exactamente una secuencia de él. Si sigue CanBuildFrom
través de Builder
, verá que sería posible hacer That
realidad serían dos secuencias, pero no tendría forma de saber a qué secuencia debe ir un elemento, ya que la función parcial solo puede decir "sí, yo". pertenecer "o" no, no pertenezco ".
Entonces, ¿qué haces si quieres tener múltiples condiciones que den lugar a que tu lista se divida en varias piezas diferentes? Una forma es crear una función de indicador A => Int
, donde su A
se asigna a una clase numerada, y luego usar groupBy
. Por ejemplo:
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)))
Ahora puede buscar sus sublistas por clase (0, 1 y 2 en este caso). Desafortunadamente, si quiere ignorar algunas entradas, igual debe ponerlas en una clase (por ejemplo, en este caso probablemente no le importen las múltiples copias de None
).
¿Es posible usar una llamada para collect
y hacer 2 nuevas listas? Si no, ¿cómo puedo hacer esto usando una partition
?
No estoy seguro de cómo hacerlo con collect
sin usar listas mutables, pero la partition
puede usar la coincidencia de patrones (solo un poco más detallada)
List("a", 1, 2, "b", 19).partition {
case s:String => true
case _ => false
}
No pude encontrar una solución satisfactoria a este problema básico aquí. No necesito una conferencia sobre collect
y no me importa si esta es la tarea de alguien. Además, no quiero algo que funcione solo para List
.
Así que aquí está mi puñalada. Eficiente y compatible con cualquier TraversableOnce
, incluso cadenas:
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
}
}
Ejemplos de uso:
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])
Yo uso esto. Una cosa buena de esto es que combina partición y mapeo en una iteración. Un inconveniente es que asigna un grupo de objetos temporales (las instancias Either.Left
y 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)
collect
(definido en TraversableLike y disponible en todas las subclases) funciona con una colección y una función PartialFunction
. También ocurre que un grupo de cláusulas de casos definidas dentro de llaves son una función parcial (consulte la sección 8.5 de la Especificación del lenguaje Scala [advertencia - PDF] )
Como en el manejo de excepciones:
try {
... do something risky ...
} catch {
//The contents of this catch block are a partial function
case e: IOException => ...
case e: OtherException => ...
}
Es una forma práctica de definir una función que solo aceptará algunos valores de un tipo determinado.
Considere usarlo en una lista de valores mixtos:
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
}
El argumento para collect
método es una función PartialFunction[Any,String]
. PartialFunction
porque no está definido para todas las entradas posibles de tipo Any
(que es el tipo de la List
) y String
porque eso es lo que devuelven todas las cláusulas.
Si intenta utilizar el map
lugar de collect
, el valor doble al final de mixedList
provocaría un MatchError
. El uso de collect
solo descarta esto, así como cualquier otro valor para el cual la función parcial no está definida.
Un posible uso sería aplicar lógica diferente a los elementos de la lista:
var strings = List.empty[String]
var ints = List.empty[Int]
mixedList collect {
case s: String => strings :+= s
case i: Int => ints :+= i
}
Aunque esto es solo un ejemplo, el uso de variables mutables como este es considerado por muchos como un crimen de guerra. ¡Por favor, no lo hagas!
Una solución mucho mejor es usar collect dos veces:
val strings = mixedList collect { case s: String => s }
val ints = mixedList collect { case i: Int => i }
O si sabe con certeza que la lista solo contiene dos tipos de valores, puede usar la partition
, que divide las colecciones en valores según si coinciden o no con algún predicado:
//if the list only contains Strings and Ints:
val (strings, ints) = mixedList partition { case s: String => true; case _ => false }
El truco aquí es que las strings
y de entrada son de tipo List[Any]
, aunque puede forzarlos a volver a algo más seguro (quizás mediante el uso de collect
...)
Si ya tiene una colección segura para tipos y desea dividir en alguna otra propiedad de los elementos, entonces las cosas son un poco más fáciles para usted:
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
Espero que resuma cómo los dos métodos pueden ayudarte aquí.