Einer der mächtigsten Muster in Scala ist die Anreicherungs-my-Bibliothek * Muster, das implizite Konvertierungen verwendet zu erscheinen , ohne dass dynamische Methode Auflösung zu bestehenden Klassen hinzuzufügen Methoden. Wenn wir zum Beispiel wünschen, dass alle Zeichenfolgen die Methode haben spaces
, mit der gezählt wird, wie viele Leerzeichen sie haben, können wir:
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
Leider stößt dieses Muster beim Umgang mit generischen Sammlungen auf Probleme. Beispielsweise wurde eine Reihe von Fragen zum sequentiellen Gruppieren von Elementen mit Sammlungen gestellt . Es ist nichts eingebaut, was auf einmal funktioniert. Dies scheint also ein idealer Kandidat für das Muster "Meine Bibliothek bereichern" zu verwenden, bei dem eine generische Sammlung C
und ein generischer Elementtyp verwendet werden 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
}
}
}
außer natürlich, dass es nicht funktioniert . Die REPL sagt uns:
<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
^
Es gibt zwei Probleme: Wie erhalten wir eine C[C[A]]
aus einer leeren C[A]
Liste (oder aus der Luft)? Und wie bekommen wir einen C[C[A]]
Rücken von der same +:
Linie anstelle von einem Seq[Seq[A]]
?
* Früher bekannt als pimp-my-library.
quelle
Antworten:
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:
CanBuildFrom
s, nicht direkt aus unserer Sammlung.Schauen wir uns ein Beispiel an.
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, undC[C[A]]
das sammelt alle Gruppen zusammen. Wir brauchen also zwei Builder, einen, derA
s nimmt undC[A]
s baut , und einen, derC[A]
s nimmt undC[C[A]]
s baut . WennCanBuildFrom
wir uns die Typensignatur von ansehen, sehen wirDies 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 Parametercbfcc
und auscbfc
.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 sollresult
, und sich selbst leeren und bereit sein, erneut damit zu beginnenclear
. 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ürC
einen Parameter war, den sie selbst mit demA
übergebenen Parameter füllt . Also geben wir ihm einfach den TypC
und lassen ihn daraus erstellenC[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:
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:
Hier haben wir ein Implizit hinzugefügt, das uns ein
Iterable[A]
fromC
gibt - für die meisten Sammlungen ist dies nur die Identität (z. B. istList[A]
bereits eineIterable[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 jedeD[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:
wo wir jetzt
C[A]
fürC
undC[C[A]]
für einsteckenD[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 derimplicit 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 SieVector
uns Folgendes auswählen und tun:Wir brauchen einen neuen
CanBuildFrom
, um den Aufbau eines Vektors von Strings zu handhaben (aber das ist wirklich einfach, da wir nur aufrufen müssenVector.newBuilder[String]
), und dann müssen wir alle Typen ausfüllen, damit derGroupingCollection
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:
quelle
Mit diesem Commit ist es viel einfacher, Scala-Sammlungen zu "bereichern", als es war, als Rex seine ausgezeichnete Antwort gab. In einfachen Fällen könnte es so aussehen:
was
filterMap
allenGenTraversableLike
s einen "gleichen Ergebnistyp" hinzufügt, der die Operation respektiert ,Und für das Beispiel aus der Frage sieht die Lösung nun so aus:
Beispiel einer REPL-Sitzung,
Beachten Sie erneut, dass das gleiche Prinzip des Ergebnistyps genauso beobachtet wurde, wie es
groupIdentical
direkt definiert worden wäreGenTraversableLike
.quelle
Ab diesem Zeitpunkt hat sich die magische Beschwörung geringfügig von der geändert, als Miles seine ausgezeichnete Antwort gab.
Das Folgende funktioniert, aber ist es kanonisch? Ich hoffe, einer der Kanonen wird es korrigieren. (Oder besser gesagt, Kanonen, eine der großen Kanonen.) Wenn die Ansichtsgrenze eine Obergrenze ist, verlieren Sie die Anwendung auf Array und String. Es scheint keine Rolle zu spielen, ob die Grenze GenTraversableLike oder TraversableLike ist. Mit IsTraversableLike erhalten Sie jedoch ein GenTraversableLike.
Es gibt mehr als einen Weg, eine Katze mit neun Leben zu häuten. Diese Version besagt, dass, sobald meine Quelle in ein GenTraversableLike konvertiert ist, ich das einfach tun kann, solange ich das Ergebnis aus GenTraversable erstellen kann. Ich interessiere mich nicht für meinen alten Repr.
Dieser erste Versuch beinhaltet eine hässliche Konvertierung von Repr in GenTraversableLike.
quelle