L'un des modèles les plus puissants disponibles dans Scala est le modèle enrich-my-library *, qui utilise des conversions implicites pour apparaître pour ajouter des méthodes aux classes existantes sans nécessiter de résolution de méthode dynamique. Par exemple, si nous souhaitions que toutes les chaînes aient la méthode spaces
qui compte le nombre de caractères d'espacement qu'elles contiennent, nous pourrions:
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
Malheureusement, ce modèle rencontre des problèmes lorsqu'il s'agit de collections génériques. Par exemple, un certain nombre de questions ont été posées sur le regroupement des éléments de manière séquentielle avec des collections . Il n'y a rien de intégré qui fonctionne en un seul coup, donc cela semble un candidat idéal pour le modèle enrichir ma bibliothèque en utilisant une collection générique C
et un type d'élément générique 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
}
}
}
sauf, bien sûr, ça ne marche pas . Le REPL nous dit:
<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
^
Il y a deux problèmes: comment obtenir un à C[C[A]]
partir d'une C[A]
liste vide (ou à partir de rien)? Et comment pouvons-nous obtenir un C[C[A]]
retour de lasame +:
ligne au lieu d'un Seq[Seq[A]]
?
* Anciennement connu sous le nom de pimp-my-library.
la source
Réponses:
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:
CanBuildFrom
s implicites , pas directement de notre collection.Regardons un exemple.
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, etC[C[A]]
cela rassemble tous les groupes. Ainsi, nous avons besoin de deux générateurs, l'un qui prendA
s et construitC[A]
s, et l'autre qui prendC[A]
s et construitC[C[A]]
s. En regardant la signature de type deCanBuildFrom
, nous voyonsce 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 implicitescbfcc
etcbfc
.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 à recommencerclear
. 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 pourC
avec un paramètre, qu'elle remplit elle-même avec leA
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:
Ç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:
Ici, nous avons ajouté un implicite qui nous donne un
Iterable[A]
fromC
- pour la plupart des collections, ce ne sera que l'identité (par exempleList[A]
déjà unIterable[A]
), mais pour les tableaux, ce sera une véritable conversion implicite. Et, par conséquent, nous avons supprimé l'exigence selonC[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 - auC[C[A]]
lieu de cela, c'est n'importeD[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:
où maintenant nous nous connectons
C[A]
pourC
etC[C[A]]
pourD[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. ChoisissonsVector
et faisons ce qui suit: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 leGroupingCollection
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:
la source
A partir de ce commit, il est beaucoup plus facile «d'enrichir» les collections Scala que lorsque Rex a donné son excellente réponse. Pour les cas simples, cela pourrait ressembler à ceci,
qui ajoute un "même type de résultat" respectant l'
filterMap
opération à tous lesGenTraversableLike
s,Et pour l'exemple de la question, la solution ressemble maintenant à:
Exemple de session REPL,
Encore une fois, notez que le même principe de type de résultat a été observé exactement de la même manière qu'il aurait
groupIdentical
été directement définiGenTraversableLike
.la source
À partir de ce commit l'incantation magique est légèrement modifiée par rapport à ce qu'elle était lorsque Miles a donné son excellente réponse.
Les œuvres suivantes, mais est-ce canonique? J'espère que l'un des canons le corrigera. (Ou plutôt, les canons, l'un des gros canons.) Si la limite de vue est une limite supérieure, vous perdez l'application à Array et String. Peu importe si la borne est GenTraversableLike ou TraversableLike; mais IsTraversableLike vous donne un GenTraversableLike.
Il y a plus d'une façon d'écorcher un chat avec neuf vies. Cette version dit qu'une fois que ma source est convertie en GenTraversableLike, tant que je peux construire le résultat à partir de GenTraversable, faites-le. Je ne suis pas intéressé par mon ancien Repr.
Cette première tentative comprend une vilaine conversion de Repr en GenTraversableLike.
la source