Um dos padrões mais poderosos disponíveis em Scala é o padrão enriquecer minha biblioteca *, que usa conversões implícitas para aparentar adicionar métodos a classes existentes sem exigir resolução de método dinâmico. Por exemplo, se desejássemos que todas as strings tivessem o método spaces
que contasse quantos caracteres de espaço em branco elas tinham, poderí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
Infelizmente, esse padrão tem problemas ao lidar com coleções genéricas. Por exemplo, várias perguntas foram feitas sobre o agrupamento de itens sequencialmente com coleções . Não há nada integrado que funcione de uma só vez, então este parece um candidato ideal para o padrão enriquecer minha biblioteca usando uma coleção genérica C
e um 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
}
}
}
exceto, é claro, que não funciona . O REPL nos diz:
<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
^
Existem dois problemas: como obtemos um C[C[A]]
de uma C[A]
lista vazia (ou do nada)? E como obtemos um C[C[A]]
retorno da same +:
linha em vez de um Seq[Seq[A]]
?
* Anteriormente conhecido como pimp-my-library.
fonte
Respostas:
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:
CanBuildFrom
s implícitos , não diretamente de nossa coleção.Vejamos um exemplo.
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, eC[C[A]]
que reúne todos os grupos. Portanto, precisamos de dois construtores, um que obtém seA
constrói seC[A]
e outro que obtém seC[A]
constróiC[C[A]]
s. Olhando para a assinatura de tipo deCanBuildFrom
, vemoso 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ícitoscbfcc
ecbfc
.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 estarresult
, esvaziar-se e estar pronto para começar de novoclear
. 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 paraC
com um parâmetro, que ela própria preenche com oA
passado para ela. Então, simplesmente entregamos o tipoC
e o deixamos criar aC[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:
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:
Aqui, adicionamos um implícito que nos dá um
Iterable[A]
fromC
--para a maioria das coleções, essa será apenas a identidade (por exemplo,List[A]
já é umIterable[A]
), mas para matrizes será uma conversão implícita real. E, conseqüentemente, deixamos de lado o requisito de queC[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 é - aoC[C[A]]
invés, é qualquerD[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:
onde agora ligamos
C[A]
paraC
eC[C[A]]
paraD[C]
. Observe que precisamos dos tipos genéricos explícitos na chamada paranew GroupingCollection
para que possamos determinar quais tipos correspondem a quais. Graças aoimplicit 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 escolherVector
e fazer o seguinte: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 chamarVector.newBuilder[String]
), e então precisamos preencher todos os tipos para que oGroupingCollection
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:
fonte
A partir desse commit , é muito mais fácil "enriquecer" as coleções do Scala do que quando Rex deu sua excelente resposta. Para casos simples, pode ser assim,
que adiciona um "mesmo tipo de resultado" respeitando a
filterMap
operação para todos osGenTraversableLike
s,E para o exemplo da pergunta, a solução agora parece,
Sessão REPL de amostra,
Novamente, observe que o mesmo princípio de tipo de resultado foi observado exatamente da mesma maneira que teria
groupIdentical
sido definido diretamente emGenTraversableLike
.fonte
A partir deste commit, o encantamento mágico é ligeiramente alterado em relação ao que era quando Miles deu sua excelente resposta.
O seguinte funciona, mas é canônico? Espero que um dos cânones o corrija. (Ou melhor, canhões, uma das grandes armas.) Se o limite da vista for um limite superior, você perderá a aplicação para Array e String. Não parece importar se o limite é GenTraversableLike ou TraversableLike; mas IsTraversableLike oferece um GenTraversableLike.
Há mais de uma maneira de esfolar um gato com nove vidas. Esta versão diz que, uma vez que minha fonte seja convertida em GenTraversableLike, contanto que eu possa construir o resultado de GenTraversable, basta fazer isso. Não estou interessado no meu antigo Repr.
Essa primeira tentativa inclui uma conversão feia de Repr em GenTraversableLike.
fonte