Der Schlüssel zum Verständnis dieses Problems besteht darin, zu erkennen, dass es zwei verschiedene Möglichkeiten gibt, Sammlungen in der Sammlungsbibliothek zu erstellen und damit zu arbeiten . Eine davon ist die Schnittstelle für öffentliche Sammlungen mit all ihren netten Methoden. Die andere, die beim Erstellen ausgiebig verwendet wird der Sammlungsbibliothek häufig verwendet wird, aber außerhalb der Bibliothek fast nie verwendet wird, sind die Builder.
Unser Problem bei der Anreicherung ist genau das gleiche, mit dem die Sammlungsbibliothek selbst konfrontiert ist, wenn versucht wird, Sammlungen desselben Typs zurückzugeben. Das heißt, wir möchten Sammlungen erstellen, aber wenn wir generisch arbeiten, haben wir keine Möglichkeit, auf "denselben Typ zu verweisen, den die Sammlung bereits hat". Wir brauchen also Bauherren .
Die Frage ist nun: Woher bekommen wir unsere Bauherren? Der offensichtliche Ort stammt aus der Sammlung selbst. Das funktioniert nicht . Wir haben bereits beim Umzug in eine generische Sammlung beschlossen, den Typ der Sammlung zu vergessen. Obwohl die Sammlung einen Builder zurückgeben könnte, der mehr Sammlungen des gewünschten Typs generieren würde, würde sie nicht wissen, um welchen Typ es sich handelt.
Stattdessen erhalten wir unsere Erbauer von CanBuildFrom
Implikits, die herumschweben. Diese dienen speziell dazu, Eingabe- und Ausgabetypen abzugleichen und Ihnen einen entsprechend typisierten Builder zu bieten.
Wir müssen also zwei konzeptionelle Sprünge machen:
- Wir verwenden keine Standard-Sammlungsoperationen, sondern Builder.
- Wir erhalten diese Builder aus impliziten
CanBuildFrom
s, nicht direkt aus unserer Sammlung.
Schauen wir uns ein Beispiel an.
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)
}
Nehmen wir das auseinander. Erstens wissen wir, dass wir zwei Arten von Sammlungen erstellen müssen, um die Sammlung von Sammlungen zu erstellen: C[A]
für jede Gruppe, und C[C[A]]
das sammelt alle Gruppen zusammen. Wir brauchen also zwei Builder, einen, der A
s nimmt und C[A]
s baut , und einen, der C[A]
s nimmt und C[C[A]]
s baut . Wenn CanBuildFrom
wir uns die Typensignatur von ansehen, sehen wir
CanBuildFrom[-From, -Elem, +To]
Dies bedeutet, dass CanBuildFrom wissen möchte, mit welcher Art von Sammlung wir beginnen - in unserem Fall ist dies der Fall C[A]
, und dann die Elemente der generierten Sammlung und der Typ dieser Sammlung. Also füllen wir diese als implizite Parameter cbfcc
und aus cbfc
.
Nachdem ich das erkannt habe, ist das der größte Teil der Arbeit. Wir können unsere CanBuildFrom
s verwenden, um uns Bauherren zu geben (alles, was Sie tun müssen, ist sie anzuwenden). Und ein Builder kann eine Sammlung mit erstellen +=
, sie in die Sammlung konvertieren, mit der er letztendlich zusammen sein soll result
, und sich selbst leeren und bereit sein, erneut damit zu beginnen clear
. Die Builder beginnen leer, wodurch unser erster Kompilierungsfehler behoben wird. Da wir Builder anstelle von Rekursion verwenden, verschwindet auch der zweite Fehler.
Ein letztes kleines Detail - abgesehen von dem Algorithmus, der die Arbeit tatsächlich erledigt - ist die implizite Konvertierung. Beachten Sie, dass wir new GroupingCollection[A,C]
nicht verwenden [A,C[A]]
. Dies liegt daran, dass die Klassendeklaration für C
einen Parameter war, den sie selbst mit dem A
übergebenen Parameter füllt . Also geben wir ihm einfach den Typ C
und lassen ihn daraus erstellen C[A]
. Kleinere Details, aber Sie erhalten Fehler bei der Kompilierung, wenn Sie einen anderen Weg versuchen.
Hier habe ich die Methode etwas allgemeiner gestaltet als die Sammlung "Gleiche Elemente". Stattdessen schneidet die Methode die ursprüngliche Sammlung auseinander, wenn der Test sequentieller Elemente fehlschlägt.
Lassen Sie uns unsere Methode in Aktion sehen:
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))
Es klappt!
Das einzige Problem ist, dass wir diese Methoden im Allgemeinen nicht für Arrays zur Verfügung haben, da dies zwei implizite Konvertierungen hintereinander erfordern würde. Es gibt verschiedene Möglichkeiten, dies zu umgehen, einschließlich des Schreibens einer separaten impliziten Konvertierung für Arrays, des Castings in WrappedArray
usw.
Edit: Mein bevorzugter Ansatz für den Umgang mit Arrays und Strings und dies ist der Code selbst zu machen mehr Generika und dann entsprechende implizite Konvertierungen verwenden sie präziser wieder so zu machen , dass Arrays auch funktionieren. In diesem speziellen Fall:
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
}
}
Hier haben wir ein Implizit hinzugefügt, das uns ein Iterable[A]
from C
gibt - für die meisten Sammlungen ist dies nur die Identität (z. B. ist List[A]
bereits eine Iterable[A]
), aber für Arrays ist es eine echte implizite Konvertierung. Infolgedessen haben wir die Anforderung fallen gelassen, dass - C[A] <: Iterable[A]
wir im Grunde nur die Anforderung für <%
explizit festgelegt haben, damit wir sie nach Belieben explizit verwenden können, anstatt sie vom Compiler für uns ausfüllen zu lassen. Außerdem haben wir die Einschränkung gelockert, dass unsere Sammlung von Sammlungen lautet - C[C[A]]
stattdessen ist es jede D[C]
, die wir später ausfüllen werden, um das zu sein, was wir wollen. Da wir dies später ausfüllen werden, haben wir es auf die Klassenebene anstatt auf die Methodenebene verschoben. Ansonsten ist es im Grunde das gleiche.
Nun ist die Frage, wie man das benutzt. Für reguläre Sammlungen können wir:
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)
}
wo wir jetzt C[A]
für C
und C[C[A]]
für einstecken D[C]
. Beachten Sie, dass wir die expliziten generischen Typen für den Aufruf benötigen, new GroupingCollection
damit klar bleibt, welche Typen welchen entsprechen. Dank der implicit c2i: C[A] => Iterable[A]
werden Arrays automatisch verarbeitet.
Aber warte, was ist, wenn wir Strings verwenden wollen? Jetzt sind wir in Schwierigkeiten, weil Sie keine "Zeichenfolge" haben können. Hier hilft die zusätzliche Abstraktion: Wir können D
etwas nennen , das zum Halten von Strings geeignet ist. Lassen Sie Vector
uns Folgendes auswählen und tun:
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
)
}
Wir brauchen einen neuen CanBuildFrom
, um den Aufbau eines Vektors von Strings zu handhaben (aber das ist wirklich einfach, da wir nur aufrufen müssen Vector.newBuilder[String]
), und dann müssen wir alle Typen ausfüllen, damit der GroupingCollection
sinnvoll eingegeben wird. Beachten Sie, dass wir bereits ein [String,Char,String]
CanBuildFrom haben, sodass Zeichenfolgen aus Zeichensammlungen erstellt werden können.
Probieren wir es aus:
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, !!)