La chiave per comprendere questo problema è rendersi conto che esistono due modi diversi per creare e lavorare con le raccolte nella libreria delle raccolte. Uno è l'interfaccia delle collezioni pubbliche con tutti i suoi metodi simpatici. L'altro, che è ampiamente utilizzato nella creazione della libreria delle collezioni, ma che non viene quasi mai utilizzato al di fuori di essa, sono i costruttori.
Il nostro problema nell'arricchimento è esattamente lo stesso che la libreria stessa deve affrontare quando si cerca di restituire raccolte dello stesso tipo. Cioè, vogliamo creare raccolte, ma quando si lavora in modo generico, non abbiamo un modo per fare riferimento allo "stesso tipo che la raccolta è già". Quindi abbiamo bisogno di costruttori .
Ora la domanda è: da dove prendiamo i nostri costruttori? Il posto ovvio è dalla collezione stessa. Questo non funziona . Abbiamo già deciso, passando a una raccolta generica, che avremmo dimenticato il tipo di raccolta. Quindi, anche se la raccolta potrebbe restituire un builder che genererebbe più raccolte del tipo desiderato, non saprebbe quale fosse il tipo.
Invece, otteniamo i nostri costruttori da CanBuildFrom
impliciti che fluttuano intorno. Questi esistono specificamente allo scopo di abbinare i tipi di input e di output e di fornire un generatore digitato in modo appropriato.
Quindi, abbiamo due salti concettuali da fare:
- Non stiamo usando operazioni di raccolta standard, stiamo usando builder.
- Otteniamo questi builder da implicit
CanBuildFrom
, non direttamente dalla nostra raccolta.
Diamo un'occhiata a un esempio.
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)
}
Smontiamo questo. Innanzitutto, per creare la raccolta di raccolte, sappiamo che avremo bisogno di creare due tipi di raccolte: C[A]
per ogni gruppo, e C[C[A]]
questo riunisce tutti i gruppi insieme. Quindi, abbiamo bisogno di due builder, uno che prende se A
costruisce se C[A]
uno che prende se C[A]
costruisce C[C[A]]
s. Guardando il tipo di firma di CanBuildFrom
, vediamo
CanBuildFrom[-From, -Elem, +To]
il che significa che CanBuildFrom vuole conoscere il tipo di raccolta con cui stiamo iniziando - nel nostro caso, è C[A]
, e quindi gli elementi della raccolta generata e il tipo di quella raccolta. Quindi li inseriamo come parametri impliciti cbfcc
e cbfc
.
Avendo capito questo, è la maggior parte del lavoro. Possiamo usare i nostri CanBuildFrom
per darci costruttori (tutto ciò che devi fare è applicarli). E un costruttore può creare una raccolta con +=
, convertirla nella raccolta con cui dovrebbe essere alla fine result
, svuotarsi ed essere pronto per ricominciare clear
. I builder iniziano vuoti, il che risolve il nostro primo errore di compilazione, e poiché stiamo usando i builder invece della ricorsione, anche il secondo errore scompare.
Un ultimo piccolo dettaglio - oltre all'algoritmo che effettivamente fa il lavoro - è nella conversione implicita. Nota che usiamo new GroupingCollection[A,C]
not [A,C[A]]
. Questo perché la dichiarazione della classe era perC
con un parametro, che lo riempie da solo con il A
passato. Quindi gli diamo solo il tipo C
e lasciamo che si crei C[A]
da esso. Dettagli minori, ma se provi in un altro modo otterrai errori in fase di compilazione.
Qui, ho reso il metodo un po 'più generico rispetto alla raccolta "elementi uguali" - piuttosto, il metodo taglia a parte la raccolta originale ogni volta che il suo test di elementi sequenziali fallisce.
Vediamo il nostro metodo in azione:
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))
Funziona!
L'unico problema è che in generale non abbiamo questi metodi disponibili per gli array, poiché ciò richiederebbe due conversioni implicite di seguito. Esistono diversi modi per aggirare questo problema, inclusa la scrittura di una conversione implicita separata per gli array, il casting WrappedArray
e così via.
Modifica: il mio approccio preferito per trattare array e stringhe è quello di rendere il codice ancora più generico e quindi utilizzare conversioni implicite appropriate per renderle di nuovo più specifiche in modo tale che anche gli array funzionino. In questo caso particolare:
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
}
}
Qui abbiamo aggiunto un implicito che ci dà un Iterable[A]
da - C
per la maggior parte delle raccolte questa sarà solo l'identità (ad esempio, List[A]
è già un Iterable[A]
), ma per gli array sarà una vera conversione implicita. E, di conseguenza, abbiamo abbandonato il requisito che C[A] <: Iterable[A]
- fondamentalmente abbiamo appena reso <%
esplicito il requisito , quindi possiamo usarlo esplicitamente a nostro piacimento invece di farlo compilare dal compilatore per noi. Inoltre, abbiamo allentato la restrizione secondo cui la nostra raccolta di raccolte è C[C[A]]
, invece, è qualsiasi D[C]
, che riempiremo in seguito per essere ciò che vogliamo. Poiché lo riempiremo più tardi, lo abbiamo spostato al livello di classe invece che a livello di metodo. Altrimenti, è fondamentalmente lo stesso.
Ora la domanda è come usarlo. Per le collezioni regolari, possiamo:
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)
}
dove ora ci colleghiamo C[A]
per C
e C[C[A]]
per D[C]
. Nota che abbiamo bisogno dei tipi generici espliciti nella chiamata a in new GroupingCollection
modo che possa mantenere chiaro quali tipi corrispondono a cosa. Grazie a implicit c2i: C[A] => Iterable[A]
, questo gestisce automaticamente gli array.
Ma aspetta, e se volessimo usare le stringhe? Adesso siamo nei guai, perché non puoi avere una "stringa di stringhe". È qui che l'astrazione extra aiuta: possiamo chiamare D
qualcosa che è adatto a contenere stringhe. Scegliamo Vector
e facciamo quanto segue:
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
)
}
Abbiamo bisogno di un nuovo CanBuildFrom
per gestire la costruzione di un vettore di stringhe (ma questo è davvero facile, dato che dobbiamo solo chiamare Vector.newBuilder[String]
), quindi dobbiamo compilare tutti i tipi in modo che GroupingCollection
sia digitato in modo ragionevole. Nota che abbiamo già fluttuato intorno a un file[String,Char,String]
fluttuato CanBuildFrom, quindi le stringhe possono essere create da raccolte di caratteri.
Proviamolo:
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, !!)