A chave para entender esse problema é perceber que existem duas maneiras diferentes de construir e trabalhar com coleções na biblioteca de coleções. Um é a interface de coleções públicas com todos os seus métodos interessantes. O outro, que é amplamente utilizado na criação da biblioteca de coleções, mas que quase nunca é usado fora dela, são os construtores.
Nosso problema de enriquecimento é exatamente o mesmo que a própria biblioteca de coleções enfrenta ao tentar retornar coleções do mesmo tipo. Ou seja, queremos construir coleções, mas ao trabalhar genericamente, não temos como nos referir a "o mesmo tipo que a coleção já é". Portanto, precisamos de construtores .
Agora a questão é: de onde tiramos nossos construtores? O lugar óbvio é na própria coleção. Isso não funciona . Já decidimos, ao mudar para uma coleção genérica, que esqueceríamos o tipo de coleção. Portanto, embora a coleção pudesse retornar um construtor que geraria mais coleções do tipo que desejamos, ela não saberia qual era o tipo.
Em vez disso, obtemos nossos construtores de CanBuildFrom
implícitos que estão flutuando. Eles existem especificamente para o propósito de combinar os tipos de entrada e saída e fornecer a você um construtor apropriadamente tipado.
Portanto, temos dois saltos conceituais a dar:
- Não estamos usando operações de coleções padrão, estamos usando construtores.
- Obtemos esses construtores de
CanBuildFrom
s implícitos , não diretamente de nossa coleção.
Vejamos um exemplo.
class GroupingCollection[A, C[A] <: Iterable[A]](ca: C[A]) {
import collection.generic.CanBuildFrom
def groupedWhile(p: (A,A) => Boolean)(
implicit cbfcc: CanBuildFrom[C[A],C[A],C[C[A]]], cbfc: CanBuildFrom[C[A],A,C[A]]
): C[C[A]] = {
val it = ca.iterator
val cca = cbfcc()
if (!it.hasNext) cca.result
else {
val as = cbfc()
var olda = it.next
as += olda
while (it.hasNext) {
val a = it.next
if (p(olda,a)) as += a
else { cca += as.result; as.clear; as += a }
olda = a
}
cca += as.result
}
cca.result
}
}
implicit def iterable_has_grouping[A, C[A] <: Iterable[A]](ca: C[A]) = {
new GroupingCollection[A,C](ca)
}
Vamos desmontar isso. Primeiro, para construir a coleção de coleções, sabemos que precisaremos construir dois tipos de coleções: C[A]
para cada grupo, e C[C[A]]
que reúne todos os grupos. Portanto, precisamos de dois construtores, um que obtém se A
constrói se C[A]
e outro que obtém se C[A]
constrói C[C[A]]
s. Olhando para a assinatura de tipo de CanBuildFrom
, vemos
CanBuildFrom[-From, -Elem, +To]
o que significa que CanBuildFrom deseja saber com que tipo de coleção estamos começando - em nosso caso, é C[A]
, e então os elementos da coleção gerada e o tipo dessa coleção. Então, nós os preenchemos como parâmetros implícitos cbfcc
e cbfc
.
Tendo percebido isso, é a maior parte do trabalho. Podemos usar nossos CanBuildFrom
s para nos fornecer construtores (tudo que você precisa fazer é aplicá-los). E um construtor pode construir uma coleção +=
, convertê-la na coleção com a qual ela deveria estar result
, esvaziar-se e estar pronto para começar de novo clear
. Os construtores começam vazios, o que resolve nosso primeiro erro de compilação e, como estamos usando construtores em vez de recursão, o segundo erro também desaparece.
Um último pequeno detalhe - além do algoritmo que realmente faz o trabalho - está na conversão implícita. Note que usamos new GroupingCollection[A,C]
não [A,C[A]]
. Isso ocorre porque a declaração da classe foi para C
com um parâmetro, que ela própria preenche com o A
passado para ela. Então, simplesmente entregamos o tipo C
e o deixamos criar a C[A]
partir dele. Pequenos detalhes, mas você obterá erros de tempo de compilação se tentar de outra maneira.
Aqui, tornei o método um pouco mais genérico do que a coleção de "elementos iguais" - em vez disso, o método separa a coleção original sempre que seu teste de elementos sequenciais falha.
Vamos ver nosso método em ação:
scala> List(1,2,2,2,3,4,4,4,5,5,1,1,1,2).groupedWhile(_ == _)
res0: List[List[Int]] = List(List(1), List(2, 2, 2), List(3), List(4, 4, 4),
List(5, 5), List(1, 1, 1), List(2))
scala> Vector(1,2,3,4,1,2,3,1,2,1).groupedWhile(_ < _)
res1: scala.collection.immutable.Vector[scala.collection.immutable.Vector[Int]] =
Vector(Vector(1, 2, 3, 4), Vector(1, 2, 3), Vector(1, 2), Vector(1))
Funciona!
O único problema é que, em geral, não temos esses métodos disponíveis para arrays, pois isso exigiria duas conversões implícitas em uma linha. Há várias maneiras de contornar isso, incluindo escrever uma conversão implícita separada para arrays, converter para WrappedArray
e assim por diante.
Edit: Minha abordagem preferida para lidar com matrizes e strings e outros é tornar o código ainda mais genérico e, em seguida, usar as conversões implícitas apropriadas para torná-las mais específicas novamente, de modo que as matrizes também funcionem. Neste caso particular:
class GroupingCollection[A, C, D[C]](ca: C)(
implicit c2i: C => Iterable[A],
cbf: CanBuildFrom[C,C,D[C]],
cbfi: CanBuildFrom[C,A,C]
) {
def groupedWhile(p: (A,A) => Boolean): D[C] = {
val it = c2i(ca).iterator
val cca = cbf()
if (!it.hasNext) cca.result
else {
val as = cbfi()
var olda = it.next
as += olda
while (it.hasNext) {
val a = it.next
if (p(olda,a)) as += a
else { cca += as.result; as.clear; as += a }
olda = a
}
cca += as.result
}
cca.result
}
}
Aqui, adicionamos um implícito que nos dá um Iterable[A]
from C
--para a maioria das coleções, essa será apenas a identidade (por exemplo, List[A]
já é um Iterable[A]
), mas para matrizes será uma conversão implícita real. E, conseqüentemente, deixamos de lado o requisito de que C[A] <: Iterable[A]
- basicamente tornamos o requisito de <%
explícito, para que possamos usá-lo explicitamente à vontade, em vez de ter o compilador preenchê-lo para nós. Além disso, relaxamos a restrição de que nossa coleção de coleções é - ao C[C[A]]
invés, é qualquer D[C]
, que preencheremos mais tarde para ser o que queremos. Como vamos preencher isso mais tarde, nós o elevamos para o nível da classe em vez do nível do método. Caso contrário, é basicamente o mesmo.
Agora, a questão é como usar isso. Para coleções regulares, podemos:
implicit def collections_have_grouping[A, C[A]](ca: C[A])(
implicit c2i: C[A] => Iterable[A],
cbf: CanBuildFrom[C[A],C[A],C[C[A]]],
cbfi: CanBuildFrom[C[A],A,C[A]]
) = {
new GroupingCollection[A,C[A],C](ca)(c2i, cbf, cbfi)
}
onde agora ligamos C[A]
para C
e C[C[A]]
para D[C]
. Observe que precisamos dos tipos genéricos explícitos na chamada para new GroupingCollection
para que possamos determinar quais tipos correspondem a quais. Graças ao implicit c2i: C[A] => Iterable[A]
, isso lida automaticamente com arrays.
Mas espere, e se quisermos usar strings? Agora estamos com problemas, porque você não pode ter uma "seqüência de cordas". É aqui que a abstração extra ajuda: podemos chamar D
algo que seja adequado para conter strings. Vamos escolher Vector
e fazer o seguinte:
val vector_string_builder = (
new CanBuildFrom[String, String, Vector[String]] {
def apply() = Vector.newBuilder[String]
def apply(from: String) = this.apply()
}
)
implicit def strings_have_grouping(s: String)(
implicit c2i: String => Iterable[Char],
cbfi: CanBuildFrom[String,Char,String]
) = {
new GroupingCollection[Char,String,Vector](s)(
c2i, vector_string_builder, cbfi
)
}
Precisamos de um novo CanBuildFrom
para lidar com a construção de um vetor de strings (mas isso é realmente fácil, já que só precisamos chamar Vector.newBuilder[String]
), e então precisamos preencher todos os tipos para que o GroupingCollection
seja digitado de maneira sensata. Observe que já temos um [String,Char,String]
CanBuildFrom flutuando , então strings podem ser feitas de coleções de chars.
Vamos experimentar:
scala> List(true,false,true,true,true).groupedWhile(_ == _)
res1: List[List[Boolean]] = List(List(true), List(false), List(true, true, true))
scala> Array(1,2,5,3,5,6,7,4,1).groupedWhile(_ <= _)
res2: Array[Array[Int]] = Array(Array(1, 2, 5), Array(3, 5, 6, 7), Array(4), Array(1))
scala> "Hello there!!".groupedWhile(_.isLetter == _.isLetter)
res3: Vector[String] = Vector(Hello, , there, !!)