La clé pour comprendre ce problème est de réaliser qu'il existe deux façons différentes de créer et de travailler avec des collections dans la bibliothèque de collections. L'un est l'interface des collections publiques avec toutes ses méthodes intéressantes. L'autre, qui est largement utilisé dans la création la bibliothèque de collections, mais qui n'est presque jamais utilisé en dehors de celle-ci, ce sont les constructeurs.
Notre problème d'enrichissement est exactement le même que celui auquel la bibliothèque de collections elle-même est confrontée lorsqu'elle tente de renvoyer des collections du même type. Autrement dit, nous voulons créer des collections, mais lorsque nous travaillons de manière générique, nous n'avons pas de moyen de faire référence au "même type que la collection est déjà". Nous avons donc besoin de constructeurs .
Maintenant, la question est: d'où viennent nos constructeurs? L'endroit évident est la collection elle-même. Ça ne marche pas . Nous avons déjà décidé, en passant à une collection générique, que nous allions oublier le type de collection. Ainsi, même si la collection pourrait renvoyer un générateur qui générerait plus de collections du type souhaité, elle ne saurait pas quel était le type.
Au lieu de cela, nous obtenons nos constructeurs d' CanBuildFrom
implicits qui flottent. Ceux-ci existent spécifiquement dans le but de faire correspondre les types d'entrée et de sortie et de vous donner un générateur correctement typé.
Nous avons donc deux sauts conceptuels à faire:
- Nous n'utilisons pas d'opérations de collecte standard, nous utilisons des générateurs.
- Nous obtenons ces générateurs à partir de
CanBuildFrom
s implicites , pas directement de notre collection.
Regardons un exemple.
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)
}
Prenons cela à part. Tout d'abord, afin de construire la collection de collections, nous savons que nous devrons créer deux types de collections: C[A]
pour chaque groupe, et C[C[A]]
cela rassemble tous les groupes. Ainsi, nous avons besoin de deux générateurs, l'un qui prend A
s et construit C[A]
s, et l'autre qui prend C[A]
s et construit C[C[A]]
s. En regardant la signature de type de CanBuildFrom
, nous voyons
CanBuildFrom[-From, -Elem, +To]
ce qui signifie que CanBuildFrom veut connaître le type de collection avec lequel nous commençons - dans notre cas, c'est C[A]
, puis les éléments de la collection générée et le type de cette collection. Nous les remplissons donc comme des paramètres implicites cbfcc
et cbfc
.
Ayant réalisé cela, c'est l'essentiel du travail. Nous pouvons utiliser nos CanBuildFrom
s pour nous donner des constructeurs (tout ce que vous avez à faire est de les appliquer). Et un constructeur peut créer une collection avec +=
, la convertir en collection avec laquelle elle est censée appartenirresult
, se vider et être prêt à recommencer clear
. Les générateurs commencent vide, ce qui résout notre première erreur de compilation, et puisque nous utilisons des générateurs au lieu de la récursivité, la deuxième erreur disparaît également.
Un dernier petit détail - autre que l'algorithme qui fait réellement le travail - est dans la conversion implicite. Notez que nous n'utilisons new GroupingCollection[A,C]
pas [A,C[A]]
. C'est parce que la déclaration de classe était pour C
avec un paramètre, qu'elle remplit elle-même avec le A
passé. Alors on lui donne juste le typeC
et le laissons créer à C[A]
partir de celui-ci. Détails mineurs, mais vous obtiendrez des erreurs de compilation si vous essayez une autre méthode.
Ici, j'ai rendu la méthode un peu plus générique que la collection «éléments égaux» - plutôt, la méthode coupe la collection originale à chaque fois que son test des éléments séquentiels échoue.
Voyons notre méthode en action:
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))
Ça marche!
Le seul problème est que nous n'avons généralement pas ces méthodes disponibles pour les tableaux, car cela nécessiterait deux conversions implicites consécutives. Il existe plusieurs façons de contourner ce problème, notamment l'écriture d'une conversion implicite distincte pour les tableaux, la conversion en WrappedArray
, etc.
Edit: Mon approche préférée pour traiter les tableaux et les chaînes, etc. , consiste à rendre le code encore plus générique, puis à utiliser les conversions implicites appropriées pour les rendre plus spécifiques de manière à ce que les tableaux fonctionnent également. Dans ce cas particulier:
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
}
}
Ici, nous avons ajouté un implicite qui nous donne un Iterable[A]
from C
- pour la plupart des collections, ce ne sera que l'identité (par exempleList[A]
déjà un Iterable[A]
), mais pour les tableaux, ce sera une véritable conversion implicite. Et, par conséquent, nous avons supprimé l'exigence selon C[A] <: Iterable[A]
laquelle - nous avons simplement rendu l'exigence <%
explicite, afin que nous puissions l'utiliser explicitement à volonté au lieu de laisser le compilateur le remplir pour nous. De plus, nous avons assoupli la restriction selon laquelle notre collection de collections est - au C[C[A]]
lieu de cela, c'est n'importe D[C]
laquelle, que nous remplirons plus tard pour être ce que nous voulons. Parce que nous allons le remplir plus tard, nous l'avons poussé au niveau de la classe au lieu du niveau de la méthode. Sinon, c'est fondamentalement la même chose.
Maintenant, la question est de savoir comment l'utiliser. Pour les collections régulières, nous pouvons:
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)
}
où maintenant nous nous connectons C[A]
pour C
et C[C[A]]
pour D[C]
. Notez que nous avons besoin des types génériques explicites lors de l'appel à new GroupingCollection
afin de pouvoir savoir quels types correspondent à quoi. Grâce à implicit c2i: C[A] => Iterable[A]
, cela gère automatiquement les tableaux.
Mais attendez, que faire si nous voulons utiliser des chaînes? Maintenant, nous avons des problèmes, car vous ne pouvez pas avoir de "chaîne de chaînes". C'est là que l'abstraction supplémentaire aide: nous pouvons appeler D
quelque chose qui convient pour contenir des chaînes. Choisissons Vector
et faisons ce qui suit:
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
)
}
Nous avons besoin d'un nouveau CanBuildFrom
pour gérer la construction d'un vecteur de chaînes (mais c'est vraiment facile, car il suffit d'appelerVector.newBuilder[String]
), puis nous devons remplir tous les types pour que le GroupingCollection
soit typé judicieusement. Notez que nous avons déjà flottant autour d'un[String,Char,String]
CanBuildFrom, de sorte que les chaînes peuvent être créées à partir de collections de caractères.
Essayons-le:
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, !!)