Uno de los patrones más poderosos disponibles en Scala es el patrón enrich-my-library *, que usa conversiones implícitas para parecer que agregan métodos a clases existentes sin requerir una resolución dinámica de métodos. Por ejemplo, si quisiéramos que todas las cadenas tuvieran el método spaces
que contara cuántos caracteres de espacios en blanco tenían, podríamos:
class SpaceCounter(s: String) {
def spaces = s.count(_.isWhitespace)
}
implicit def string_counts_spaces(s: String) = new SpaceCounter(s)
scala> "How many spaces do I have?".spaces
res1: Int = 5
Desafortunadamente, este patrón tiene problemas cuando se trata de colecciones genéricas. Por ejemplo, se han formulado varias preguntas sobre la agrupación de elementos secuencialmente con colecciones . No hay nada integrado que funcione de una sola vez, por lo que parece un candidato ideal para el patrón enriquecer mi biblioteca utilizando una colección genérica C
y un tipo de elemento genérico A
:
class SequentiallyGroupingCollection[A, C[A] <: Seq[A]](ca: C[A]) {
def groupIdentical: C[C[A]] = {
if (ca.isEmpty) C.empty[C[A]]
else {
val first = ca.head
val (same,rest) = ca.span(_ == first)
same +: (new SequentiallyGroupingCollection(rest)).groupIdentical
}
}
}
excepto, por supuesto, que no funciona . El REPL nos dice:
<console>:12: error: not found: value C
if (ca.isEmpty) C.empty[C[A]]
^
<console>:16: error: type mismatch;
found : Seq[Seq[A]]
required: C[C[A]]
same +: (new SequentiallyGroupingCollection(rest)).groupIdentical
^
Hay dos problemas: ¿cómo obtenemos un C[C[A]]
de una C[A]
lista vacía (o de la nada)? ¿Y cómo obtenemos un C[C[A]]
respaldo de la same +:
línea en lugar de un Seq[Seq[A]]
?
* Anteriormente conocido como pimp-my-library.
fuente
Respuestas:
La clave para comprender este problema es darse cuenta de que hay dos formas diferentes de construir y trabajar con colecciones en la biblioteca de colecciones. Uno es la interfaz de colecciones públicas con todos sus buenos métodos. El otro, que se utiliza ampliamente en la creación de la biblioteca de colecciones, pero que casi nunca se utiliza fuera de ella, son los constructores.
Nuestro problema de enriquecimiento es exactamente el mismo que enfrenta la propia biblioteca de colecciones cuando intenta devolver colecciones del mismo tipo. Es decir, queremos construir colecciones, pero cuando trabajamos de forma genérica, no tenemos forma de referirnos al "mismo tipo que la colección ya es". Entonces necesitamos constructores .
Ahora la pregunta es: ¿de dónde sacamos a nuestros constructores? El lugar obvio es de la propia colección. Esto no funciona . Ya decidimos, al pasar a una colección genérica, que nos íbamos a olvidar del tipo de colección. Entonces, aunque la colección podría devolver un constructor que generaría más colecciones del tipo que queremos, no sabría cuál era el tipo.
En cambio, obtenemos nuestros constructores de los
CanBuildFrom
implícitos que flotan alrededor. Estos existen específicamente con el propósito de hacer coincidir los tipos de entrada y salida y brindarle un constructor debidamente tipificado.Entonces, tenemos dos saltos conceptuales que dar:
CanBuildFrom
s implícitos , no directamente de nuestra colección.Veamos un ejemplo.
Desarmemos esto. Primero, para construir la colección de colecciones, sabemos que necesitaremos construir dos tipos de colecciones:
C[A]
para cada grupo, yC[C[A]]
eso reúne a todos los grupos. Por lo tanto, necesitamos dos constructores, uno que tomaA
sy construyeC[A]
s, y otro que tomaC[A]
sy construyeC[C[A]]
s. Mirando la firma de tipo deCanBuildFrom
, vemoslo que significa que CanBuildFrom quiere saber el tipo de colección con la que estamos comenzando, en nuestro caso, es
C[A]
, y luego los elementos de la colección generada y el tipo de esa colección. Así que los completamos como parámetros implícitoscbfcc
ycbfc
.Habiéndome dado cuenta de esto, eso es la mayor parte del trabajo. Podemos usar nuestros
CanBuildFrom
s para darnos constructores (todo lo que necesita hacer es aplicarlos). Y un constructor puede crear una colección+=
, convertirla en la colección con la que se supone que debe estar en última instanciaresult
, vaciarse y estar listo para empezar de nuevoclear
. Los constructores comienzan vacíos, lo que resuelve nuestro primer error de compilación, y dado que estamos usando constructores en lugar de recursividad, el segundo error también desaparece.Un último pequeño detalle, además del algoritmo que realmente hace el trabajo, está en la conversión implícita. En cuenta que utilizamos
new GroupingCollection[A,C]
no[A,C[A]]
. Esto se debe a que la declaración de la clase fue paraC
con un parámetro, que lo llena él mismo con elA
pasado. Así que le damos el tipoC
y dejamos que se cree aC[A]
partir de él. Detalles menores, pero obtendrá errores en tiempo de compilación si intenta de otra manera.Aquí, he hecho el método un poco más genérico que la colección de "elementos iguales"; más bien, el método corta la colección original cada vez que falla su prueba de elementos secuenciales.
Veamos nuestro método en acción:
¡Funciona!
El único problema es que, en general, no tenemos estos métodos disponibles para matrices, ya que eso requeriría dos conversiones implícitas seguidas. Hay varias formas de solucionar este problema, incluida la escritura de una conversión implícita separada para matrices, la conversión a
WrappedArray
, etc.Editar: Mi enfoque favorito para tratar con matrices y cadenas es hacer que el código sea aún más genérico y luego usar las conversiones implícitas apropiadas para hacerlas más específicas nuevamente de tal manera que las matrices también funcionen. En este caso particular:
Aquí hemos agregado un implícito que nos da un
Iterable[A]
desdeC
- para la mayoría de las colecciones, esto será solo la identidad (por ejemplo,List[A]
ya es unIterable[A]
), pero para los arreglos será una conversión implícita real. Y, en consecuencia, hemos eliminado el requisito de que,C[A] <: Iterable[A]
básicamente, acabamos de hacer<%
explícito el requisito para que podamos usarlo explícitamente a voluntad en lugar de que el compilador lo complete por nosotros. Además, hemos relajado la restricción de que nuestra colección de colecciones esC[C[A]]
, en cambio, es cualquieraD[C]
, que completaremos más adelante para que sea lo que queremos. Debido a que vamos a completar esto más adelante, lo subimos al nivel de clase en lugar del nivel de método. De lo contrario, es básicamente lo mismo.Ahora la pregunta es cómo usar esto. Para colecciones regulares, podemos:
donde ahora conectamos
C[A]
paraC
yC[C[A]]
paraD[C]
. Tenga en cuenta que necesitamos los tipos genéricos explícitos en la llamada anew GroupingCollection
para poder aclarar qué tipos corresponden a qué. Gracias aimplicit c2i: C[A] => Iterable[A]
, esto maneja automáticamente las matrices.Pero espera, ¿y si queremos usar cadenas? Ahora estamos en problemas, porque no puedes tener una "cadena de cuerdas". Aquí es donde la abstracción adicional ayuda: podemos llamar a
D
algo que sea adecuado para contener cadenas. EscojamosVector
y hagamos lo siguiente:Necesitamos una nueva
CanBuildFrom
para manejar la construcción de un vector de cadenas (pero esto es realmente fácil, ya que solo necesitamos llamarVector.newBuilder[String]
), y luego necesitamos completar todos los tipos para queGroupingCollection
se escriba con sensatez. Tenga en cuenta que ya tenemos flotando alrededor de un[String,Char,String]
CanBuildFrom, por lo que las cadenas se pueden hacer a partir de colecciones de caracteres.Probémoslo:
fuente
A partir de este compromiso , es mucho más fácil "enriquecer" las colecciones de Scala que cuando Rex dio su excelente respuesta. Para casos simples, podría verse así:
que agrega un "mismo tipo de resultado" con respecto a la
filterMap
operación a todos losGenTraversableLike
s,Y para el ejemplo de la pregunta, la solución ahora se ve así,
Muestra de sesión REPL,
Nuevamente, tenga en cuenta que el mismo principio de tipo de resultado se ha observado exactamente de la misma manera en que se hubiera
groupIdentical
definido directamenteGenTraversableLike
.fuente
A partir de este compromiso el encantamiento mágico cambia ligeramente de lo que era cuando Miles dio su excelente respuesta.
Lo siguiente funciona, pero ¿es canónico? Espero que uno de los cánones lo corrija. (O más bien, cañones, una de las armas grandes). Si el límite de la vista es un límite superior, pierde la aplicación a Array y String. No parece importar si el límite es GenTraversableLike o TraversableLike; pero IsTraversableLike le da un GenTraversableLike.
Hay más de una forma de despellejar a un gato con nueve vidas. Esta versión dice que una vez que mi fuente se convierta a GenTraversableLike, siempre que pueda generar el resultado desde GenTraversable, simplemente hágalo. No estoy interesado en mi antiguo Repr.
Este primer intento incluye una conversión fea de Repr a GenTraversableLike.
fuente